diff --git a/.eslintignore b/.eslintignore index b2c4a5b6..5a8dd89e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -14,3 +14,4 @@ **/extensions/**/build/** **/extensions/markdown-language-features/media/** **/extensions/typescript-basics/test/colorize-fixtures/** +**/extensions/**/dist/** diff --git a/.eslintrc.json b/.eslintrc.json index fd72f0e5..24b5b101 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,1010 +1,1030 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint", - "jsdoc", - "mocha" - ], - "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", - "context" - ], // non-complete list of globals that are easy to access unintentionally - "no-var": "warn", - "jsdoc/no-types": "warn", - "semi": "off", - "mocha/no-exclusive-tests": "warn", - "@typescript-eslint/semi": "warn", - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "class", - "format": [ - "PascalCase" - ] - } - ], - "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": [], - "node": [ - "common" - ], - "browser": [ - "common" - ], - "electron-sandbox": [ - "common", - "browser" - ], - "electron-browser": [ - "common", - "browser", - "node", - "electron-sandbox" - ], - "electron-main": [ - "common", - "node" - ] - } - ], - "code-import-patterns": [ - "warn", - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - // !!! Do not relax these rules !!! - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - { - "target": "**/vs/base/common/**", - "restrictions": [ - "vs/nls", - "**/vs/base/common/**" - ] - }, - { - "target": "**/vs/base/test/common/**", - "restrictions": [ - "assert", - "sinon", - "vs/nls", - "**/vs/base/common/**", - "**/vs/base/test/common/**" - ] - }, - { - "target": "**/vs/base/browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/{common,browser}/**" - ] - }, - { - "target": "**/vs/base/electron-sandbox/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/{common,browser,electron-sandbox}/**" - ] - }, - { - "target": "**/vs/base/node/**", - "restrictions": [ - "vs/nls", - "**/vs/base/{common,node}/**", - "*" // node modules - ] - }, - { - // vs/base/test/browser contains tests for vs/base/browser - "target": "**/vs/base/test/browser/**", - "restrictions": [ - "assert", - "sinon", - "vs/nls", - "**/vs/base/{common,browser}/**", - "**/vs/base/test/{common,browser}/**" - ] - }, - { - "target": "**/vs/base/parts/*/common/**", - "restrictions": [ - "vs/nls", - "**/vs/base/common/**", - "**/vs/base/parts/*/common/**" - ] - }, - { - "target": "**/vs/base/parts/*/browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/{common,browser}/**", - "**/vs/base/parts/*/{common,browser}/**" - ] - }, - { - "target": "**/vs/base/parts/*/node/**", - "restrictions": [ - "vs/nls", - "**/vs/base/{common,node}/**", - "**/vs/base/parts/*/{common,node}/**", - "*" // node modules - ] - }, - { - "target": "**/vs/base/parts/*/electron-sandbox/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/{common,browser,electron-sandbox}/**", - "**/vs/base/parts/*/{common,browser,electron-sandbox}/**" - ] - }, - { - "target": "**/vs/base/parts/*/electron-browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/base/parts/*/{common,browser,node,electron-sandbox,electron-browser}/**", - "*" // node modules - ] - }, - { - "target": "**/vs/base/parts/*/electron-main/**", - "restrictions": [ - "vs/nls", - "**/vs/base/{common,node,electron-main}/**", - "**/vs/base/parts/*/{common,node,electron-main}/**", - "*" // node modules - ] - }, - { - "target": "**/vs/platform/*/common/**", - "restrictions": [ - "vs/nls", - "**/vs/base/common/**", - "**/vs/base/parts/*/common/**", - "**/vs/platform/*/common/**" - ] - }, - { - "target": "**/vs/platform/*/test/common/**", - "restrictions": [ - "assert", - "sinon", - "vs/nls", - "**/vs/base/common/**", - "**/vs/base/parts/*/common/**", - "**/vs/base/test/common/**", - "**/vs/platform/*/common/**", - "**/vs/platform/*/test/common/**" - ] - }, - { - "target": "**/vs/platform/*/browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/{common,browser}/**", - "**/vs/base/parts/*/{common,browser}/**", - "**/vs/platform/*/{common,browser}/**" - ] - }, - { - "target": "**/vs/platform/*/node/**", - "restrictions": [ - "vs/nls", - "**/vs/base/{common,node}/**", - "**/vs/base/parts/*/{common,node}/**", - "**/vs/platform/*/{common,node}/**", - "*" // node modules - ] - }, - { - "target": "**/vs/platform/*/electron-sandbox/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/{common,browser,electron-sandbox}/**", - "**/vs/base/parts/*/{common,browser,electron-sandbox}/**", - "**/vs/platform/*/{common,browser,electron-sandbox}/**" - ] - }, - { - "target": "**/vs/platform/*/electron-browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/base/parts/*/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/platform/*/{common,browser,node,electron-sandbox,electron-browser}/**", - "*" // node modules - ] - }, - { - "target": "**/vs/platform/*/electron-main/**", - "restrictions": [ - "vs/nls", - "**/vs/base/{common,node,electron-main}/**", - "**/vs/base/parts/*/{common,node,electron-main}/**", - "**/vs/platform/*/{common,node,electron-main}/**", - "**/vs/code/**", - "*" // node modules - ] - }, - { - "target": "**/vs/platform/*/test/browser/**", - "restrictions": [ - "assert", - "sinon", - "vs/nls", - "**/vs/base/{common,browser}/**", - "**/vs/platform/*/{common,browser}/**", - "**/vs/platform/*/test/{common,browser}/**" - ] - }, - { - "target": "**/vs/editor/common/**", - "restrictions": [ - "vs/nls", - "**/vs/base/common/**", - "**/vs/base/worker/**", - "**/vs/platform/*/common/**", - "**/vs/editor/common/**" - ] - }, - { - "target": "**/vs/editor/test/common/**", - "restrictions": [ - "assert", - "sinon", - "vs/nls", - "**/vs/base/common/**", - "**/vs/platform/*/common/**", - "**/vs/platform/*/test/common/**", - "**/vs/editor/common/**", - "**/vs/editor/test/common/**" - ] - }, - { - "target": "**/vs/editor/browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/{common,browser}/**", - "**/vs/platform/*/{common,browser}/**", - "**/vs/editor/{common,browser}/**" - ] - }, - { - "target": "**/vs/editor/test/browser/**", - "restrictions": [ - "assert", - "sinon", - "vs/nls", - "**/vs/base/{common,browser}/**", - "**/vs/platform/*/{common,browser}/**", - "**/vs/platform/*/test/{common,browser}/**", - "**/vs/editor/{common,browser}/**", - "**/vs/editor/test/{common,browser}/**" - ] - }, - { - "target": "**/vs/editor/standalone/common/**", - "restrictions": [ - "vs/nls", - "**/vs/base/common/**", - "**/vs/platform/*/common/**", - "**/vs/editor/common/**", - "**/vs/editor/standalone/common/**" - ] - }, - { - "target": "**/vs/editor/standalone/test/common/**", - "restrictions": [ - "assert", - "sinon", - "vs/nls", - "**/vs/base/common/**", - "**/vs/platform/*/common/**", - "**/vs/platform/*/test/common/**", - "**/vs/editor/common/**", - "**/vs/editor/test/common/**" - ] - }, - { - "target": "**/vs/editor/standalone/browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/{common,browser}/**", - "**/vs/base/parts/*/{common,browser}/**", - "**/vs/platform/*/{common,browser}/**", - "**/vs/editor/{common,browser}/**", - "**/vs/editor/contrib/**", - "**/vs/editor/standalone/{common,browser}/**" - ] - }, - { - "target": "**/vs/editor/standalone/test/browser/**", - "restrictions": [ - "assert", - "sinon", - "vs/nls", - "**/vs/base/{common,browser}/**", - "**/vs/platform/*/{common,browser}/**", - "**/vs/platform/*/test/{common,browser}/**", - "**/vs/editor/{common,browser}/**", - "**/vs/editor/standalone/{common,browser}/**", - "**/vs/editor/test/{common,browser}/**" - ] - }, - { - "target": "**/vs/editor/contrib/*/test/**", - "restrictions": [ - "assert", - "sinon", - "vs/nls", - "**/vs/base/{common,browser}/**", - "**/vs/base/test/{common,browser}/**", - "**/vs/base/parts/*/{common,browser}/**", - "**/vs/platform/*/{common,browser}/**", - "**/vs/platform/*/test/{common,browser}/**", - "**/vs/editor/{common,browser}/**", - "**/vs/editor/test/{common,browser}/**", - "**/vs/editor/contrib/**" - ] - }, - { - "target": "**/vs/editor/contrib/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/{common,browser}/**", - "**/vs/base/parts/*/{common,browser}/**", - "**/vs/platform/{common,browser}/**", - "**/vs/platform/*/{common,browser}/**", - "**/vs/editor/{common,browser}/**", - "**/vs/editor/contrib/**" - ] - }, - { - "target": "**/vs/workbench/common/**", - "restrictions": [ - "vs/nls", - "**/vs/base/common/**", - "**/vs/base/parts/*/common/**", - "**/vs/platform/*/common/**", - "**/vs/editor/common/**", - "**/vs/editor/contrib/*/common/**", - "**/vs/workbench/common/**", - "**/vs/workbench/services/*/common/**", - "assert" - ] - }, - { - "target": "**/vs/workbench/browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/{common,browser}/**", - "**/vs/base/parts/*/{common,browser}/**", - "**/vs/platform/*/{common,browser}/**", - "**/vs/editor/{common,browser}/**", - "**/vs/editor/contrib/**", // editor/contrib is equivalent to /browser/ by convention - "**/vs/workbench/workbench.web.api", - "**/vs/workbench/{common,browser}/**", - "**/vs/workbench/services/*/{common,browser}/**", - "assert" - ] - }, - { - "target": "**/vs/workbench/api/common/**", - "restrictions": [ - "vscode", - "vs/nls", - "**/vs/base/common/**", - "**/vs/platform/*/common/**", - "**/vs/editor/common/**", - "**/vs/editor/contrib/*/common/**", - "**/vs/workbench/api/common/**", - "**/vs/workbench/common/**", - "**/vs/workbench/services/*/common/**", - "**/vs/workbench/contrib/*/common/**" - ] - }, - { - "target": "**/vs/workbench/api/worker/**", - "restrictions": [ - "vscode", - "vs/nls", - "**/vs/**/{common,worker}/**" - ] - }, - { - "target": "**/vs/workbench/electron-sandbox/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/{common,browser,electron-sandbox}/**", - "**/vs/base/parts/*/{common,browser,electron-sandbox}/**", - "**/vs/platform/*/{common,browser,electron-sandbox}/**", - "**/vs/editor/{common,browser,electron-sandbox}/**", - "**/vs/editor/contrib/**", // editor/contrib is equivalent to /browser/ by convention - "**/vs/workbench/{common,browser,electron-sandbox}/**", - "**/vs/workbench/api/{common,browser,electron-sandbox}/**", - "**/vs/workbench/services/*/{common,browser,electron-sandbox}/**" - ] - }, - { - "target": "**/vs/workbench/electron-browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/base/parts/*/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/platform/*/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/editor/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/editor/contrib/**", // editor/contrib is equivalent to /browser/ by convention - "**/vs/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/workbench/api/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/workbench/services/*/{common,browser,node,electron-sandbox,electron-browser}/**", - "*" // node modules - ] - }, - { - "target": "**/vs/workbench/services/**/test/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/**", - "**/vs/platform/**", - "**/vs/editor/**", - "**/vs/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", - "vs/workbench/contrib/files/common/editors/fileEditorInput", - "**/vs/workbench/services/**", - "**/vs/workbench/test/**", - "*" // node modules - ] - }, - { - "target": "**/vs/workbench/services/**/common/**", - "restrictions": [ - "vs/nls", - "**/vs/base/**/common/**", - "**/vs/platform/**/common/**", - "**/vs/editor/common/**", - "**/vs/workbench/workbench.web.api", - "**/vs/workbench/common/**", - "**/vs/workbench/services/**/common/**", - "**/vs/workbench/api/**/common/**", - "vscode-textmate", - "vscode-oniguruma", - "iconv-lite-umd", - "tas-client-umd", - "jschardet" - ] - }, - { - "target": "**/vs/workbench/services/**/worker/**", - "restrictions": [ - "vs/nls", - "**/vs/base/**/common/**", - "**/vs/platform/**/common/**", - "**/vs/editor/common/**", - "**/vs/workbench/**/common/**", - "**/vs/workbench/**/worker/**", - "**/vs/workbench/services/**/common/**", - "vscode" - ] - }, - { - "target": "**/vs/workbench/services/**/browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/**/{common,browser,worker}/**", - "**/vs/platform/**/{common,browser}/**", - "**/vs/editor/{common,browser}/**", - "**/vs/workbench/workbench.web.api", - "**/vs/workbench/{common,browser}/**", - "**/vs/workbench/api/{common,browser}/**", - "**/vs/workbench/services/**/{common,browser}/**", - "vscode-textmate", - "vscode-oniguruma", - "iconv-lite-umd", - "jschardet" - ] - }, - { - "target": "**/vs/workbench/services/**/node/**", - "restrictions": [ - "vs/nls", - "**/vs/base/**/{common,node}/**", - "**/vs/platform/**/{common,node}/**", - "**/vs/editor/{common,node}/**", - "**/vs/workbench/{common,node}/**", - "**/vs/workbench/api/{common,node}/**", - "**/vs/workbench/services/**/{common,node}/**", - "*" // node modules - ] - }, - { - "target": "**/vs/workbench/services/**/electron-sandbox/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/**/{common,browser,worker,electron-sandbox}/**", - "**/vs/platform/**/{common,browser,electron-sandbox}/**", - "**/vs/editor/**", - "**/vs/workbench/{common,browser,electron-sandbox}/**", - "**/vs/workbench/api/{common,browser,electron-sandbox}/**", - "**/vs/workbench/services/**/{common,browser,electron-sandbox}/**", - "vscode-textmate", - "vscode-oniguruma", - "iconv-lite-umd", - "jschardet" - ] - }, - { - "target": "**/vs/workbench/services/**/electron-browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/**/{common,browser,worker,node,electron-sandbox,electron-browser}/**", - "**/vs/platform/**/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/editor/**", - "**/vs/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/workbench/api/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/workbench/services/**/{common,browser,node,electron-sandbox,electron-browser}/**", - "*" // node modules - ] - }, - { - "target": "**/vs/workbench/contrib/**/test/**", - "restrictions": [ - "assert", - "vs/nls", - "vs/css!./**/*", - "**/vs/base/**", - "**/vs/platform/**", - "**/vs/editor/**", - "**/vs/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/workbench/services/**", - "**/vs/workbench/contrib/**", - "**/vs/workbench/test/**", - "*" - ] - }, - { - "target": "**/vs/workbench/contrib/terminal/browser/**", - "restrictions": [ - // xterm and its addons are strictly browser-only components - "xterm", - "xterm-addon-*", - "vs/nls", - "vs/css!./**/*", - "**/vs/base/**/{common,browser}/**", - "**/vs/platform/**/{common,browser}/**", - "**/vs/editor/**", - "**/vs/workbench/{common,browser}/**", - "**/vs/workbench/contrib/**/{common,browser}/**", - "**/vs/workbench/services/**/{common,browser}/**" - ] - }, - { - "target": "**/vs/workbench/contrib/extensions/browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/**/{common,browser}/**", - "**/vs/platform/**/{common,browser}/**", - "**/vs/editor/**", - "**/vs/workbench/{common,browser}/**", - "**/vs/workbench/contrib/**/{common,browser}/**", - "**/vs/workbench/services/**/{common,browser}/**" - ] - }, - { - "target": "**/vs/workbench/contrib/update/browser/update.ts", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/**/{common,browser}/**", - "**/vs/platform/**/{common,browser}/**", - "**/vs/editor/**", - "**/vs/workbench/{common,browser}/**", - "**/vs/workbench/contrib/**/{common,browser}/**", - "**/vs/workbench/services/**/{common,browser}/**" - ] - }, - { - "target": "**/vs/workbench/contrib/notebook/common/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/**/{common,worker}/**", - "**/vs/platform/**/common/**", - "**/vs/editor/**", - "**/vs/workbench/common/**", - "**/vs/workbench/api/common/**", - "**/vs/workbench/services/**/common/**", - "**/vs/workbench/contrib/**/common/**" - ] - }, - { - "target": "**/vs/workbench/contrib/**/common/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/**/common/**", - "**/vs/platform/**/common/**", - "**/vs/editor/**", - "**/vs/workbench/common/**", - "**/vs/workbench/api/common/**", - "**/vs/workbench/services/**/common/**", - "**/vs/workbench/contrib/**/common/**" - ] - }, - { - "target": "**/vs/workbench/contrib/**/browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/**/{common,browser}/**", - "**/vs/platform/**/{common,browser}/**", - "**/vs/editor/**", - "**/vs/workbench/{common,browser}/**", - "**/vs/workbench/api/{common,browser}/**", - "**/vs/workbench/services/**/{common,browser}/**", - "**/vs/workbench/contrib/**/{common,browser}/**", - "vscode-textmate", - "vscode-oniguruma", - "iconv-lite-umd", - "jschardet" - ] - }, - { - "target": "**/vs/workbench/contrib/**/node/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/**/{common,node}/**", - "**/vs/platform/**/{common,node}/**", - "**/vs/editor/**/common/**", - "**/vs/workbench/{common,node}/**", - "**/vs/workbench/api/{common,node}/**", - "**/vs/workbench/services/**/{common,node}/**", - "**/vs/workbench/contrib/**/{common,node}/**", - "*" // node modules - ] - }, - { - "target": "**/vs/workbench/contrib/**/electron-sandbox/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/**/{common,browser,worker,electron-sandbox}/**", - "**/vs/platform/**/{common,browser,electron-sandbox}/**", - "**/vs/editor/**", - "**/vs/workbench/{common,browser,electron-sandbox}/**", - "**/vs/workbench/api/{common,browser,electron-sandbox}/**", - "**/vs/workbench/services/**/{common,browser,electron-sandbox}/**", - "**/vs/workbench/contrib/**/{common,browser,electron-sandbox}/**", - "vscode-textmate", - "vscode-oniguruma", - "iconv-lite-umd", - "jschardet" - ] - }, - { - "target": "**/vs/workbench/contrib/**/electron-browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/**/{common,browser,worker,node,electron-sandbox,electron-browser}/**", - "**/vs/platform/**/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/editor/**", - "**/vs/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/workbench/api/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/workbench/services/**/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/workbench/contrib/**/{common,browser,node,electron-sandbox,electron-browser}/**", - "*" // node modules - ] - }, - { - "target": "**/vs/code/browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/**/{common,browser}/**", - "**/vs/base/parts/**/{common,browser}/**", - "**/vs/platform/**/{common,browser}/**", - "**/vs/code/**/{common,browser}/**", - "**/vs/workbench/workbench.web.api" - ] - }, - { - "target": "**/vs/code/node/**", - "restrictions": [ - "vs/nls", - "**/vs/base/**/{common,node}/**", - "**/vs/base/parts/**/{common,node}/**", - "**/vs/platform/**/{common,node}/**", - "**/vs/code/**/{common,node}/**", - "*" // node modules - ] - }, - { - "target": "**/vs/code/electron-browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/**/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/base/parts/**/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/platform/**/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/code/**/{common,browser,node,electron-sandbox,electron-browser}/**", - "*" // node modules - ] - }, - { - "target": "**/vs/code/electron-main/**", - "restrictions": [ - "vs/nls", - "**/vs/base/**/{common,node,electron-main}/**", - "**/vs/base/parts/**/{common,node,electron-main}/**", - "**/vs/platform/**/{common,node,electron-main}/**", - "**/vs/code/**/{common,node,electron-main}/**", - "*" // node modules - ] - }, - { - "target": "**/vs/server/**", - "restrictions": [ - "vs/nls", - "**/vs/base/**/{common,node}/**", - "**/vs/base/parts/**/{common,node}/**", - "**/vs/platform/**/{common,node}/**", - "**/vs/workbench/**/{common,node}/**", - "**/vs/server/**", - "**/vs/code/**/{common,node}/**", - "*" // node modules - ] - }, - { - "target": "**/src/vs/workbench/workbench.common.main.ts", - "restrictions": [ - "vs/nls", - "**/vs/base/**/{common,browser}/**", - "**/vs/base/parts/**/{common,browser}/**", - "**/vs/platform/**/{common,browser}/**", - "**/vs/editor/**", - "**/vs/workbench/**/{common,browser}/**" - ] - }, - { - "target": "**/src/vs/workbench/workbench.web.main.ts", - "restrictions": [ - "vs/nls", - "**/vs/base/**/{common,browser}/**", - "**/vs/base/parts/**/{common,browser}/**", - "**/vs/platform/**/{common,browser}/**", - "**/vs/editor/**", - "**/vs/workbench/**/{common,browser}/**", - "**/vs/workbench/workbench.common.main" - ] - }, - { - "target": "**/src/vs/workbench/workbench.web.api.ts", - "restrictions": [ - "vs/nls", - "**/vs/base/**/{common,browser}/**", - "**/vs/base/parts/**/{common,browser}/**", - "**/vs/platform/**/{common,browser}/**", - "**/vs/editor/**", - "**/vs/workbench/**/{common,browser}/**", - "**/vs/workbench/workbench.web.main" - ] - }, - { - "target": "**/src/vs/workbench/workbench.sandbox.main.ts", - "restrictions": [ - "vs/nls", - "**/vs/base/**/{common,browser,electron-sandbox}/**", - "**/vs/base/parts/**/{common,browser,electron-sandbox}/**", - "**/vs/platform/**/{common,browser,electron-sandbox}/**", - "**/vs/editor/**", - "**/vs/workbench/**/{common,browser,electron-sandbox}/**", - "**/vs/workbench/workbench.common.main" - ] - }, - { - "target": "**/src/vs/workbench/workbench.desktop.main.ts", - "restrictions": [ - "vs/nls", - "**/vs/base/**/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/base/parts/**/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/platform/**/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/editor/**", - "**/vs/workbench/**/{common,browser,node,electron-sandbox,electron-browser}/**", - "**/vs/workbench/workbench.common.main", - "**/vs/workbench/workbench.sandbox.main" - ] - }, - { - "target": "**/extensions/**", - "restrictions": "**/*" - }, - { - "target": "**/test/smoke/**", - "restrictions": [ - "**/test/smoke/**", - "*" // node modules - ] - }, - { - "target": "**/test/automation/**", - "restrictions": [ - "**/test/automation/**", - "*" // node modules - ] - }, - { - "target": "**/test/integration/**", - "restrictions": [ - "**/test/integration/**", - "*" // node modules - ] - }, - { - "target": "**/api/**.test.ts", - "restrictions": [ - "**/vs/**", - "assert", - "sinon", - "crypto", - "vscode" - ] - }, - { - "target": "**/{node,electron-browser,electron-main}/**/*.test.ts", - "restrictions": [ - "**/vs/**", - "*" // node modules - ] - }, - { - "target": "**/{node,electron-browser,electron-main}/**/test/**", - "restrictions": [ - "**/vs/**", - "*" // node modules - ] - }, - { - "target": "**/test/{node,electron-browser,electron-main}/**", - "restrictions": [ - "**/vs/**", - "*" // node modules - ] - }, - { - "target": "**/**.test.ts", - "restrictions": [ - "**/vs/**", - "assert", - "sinon", - "crypto", - "xterm*" - ] - }, - { - "target": "**/test/**", - "restrictions": [ - "**/vs/**", - "assert", - "sinon", - "crypto", - "xterm*" - ] - } - ] - }, - "overrides": [ - { - "files": [ - "*.js" - ], - "rules": { - "jsdoc/no-types": "off" - } - }, - { - "files": [ - "**/vscode.d.ts", - "**/vscode.proposed.d.ts" - ], - "rules": { - "vscode-dts-create-func": "warn", - "vscode-dts-literal-or-types": "warn", - "vscode-dts-interface-naming": "warn", - "vscode-dts-event-naming": [ - "warn", - { - "allowed": [ - "onCancellationRequested", - "event" - ], - "verbs": [ - "accept", - "change", - "close", - "collapse", - "create", - "delete", - "discover", - "dispose", - "edit", - "end", - "expand", - "hide", - "open", - "override", - "receive", - "register", - "rename", - "save", - "send", - "start", - "terminate", - "trigger", - "unregister", - "write" - ] - } - ] - } - } - ] + "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", + "context" + ], // 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/naming-convention": [ + "warn", + { + "selector": "class", + "format": [ + "PascalCase" + ] + } + ], + "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": [], + "node": [ + "common" + ], + "browser": [ + "common" + ], + "electron-sandbox": [ + "common", + "browser" + ], + "electron-browser": [ + "common", + "browser", + "node", + "electron-sandbox" + ], + "electron-main": [ + "common", + "node" + ] + } + ], + "code-import-patterns": [ + "warn", + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // !!! Do not relax these rules !!! + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + { + "target": "**/vs/base/common/**", + "restrictions": [ + "vs/nls", + "**/vs/base/common/**" + ] + }, + { + "target": "**/vs/base/test/common/**", + "restrictions": [ + "assert", + "sinon", + "vs/nls", + "**/vs/base/common/**", + "**/vs/base/test/common/**" + ] + }, + { + "target": "**/vs/base/browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/{common,browser}/**" + ] + }, + { + "target": "**/vs/base/electron-sandbox/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/{common,browser,electron-sandbox}/**" + ] + }, + { + "target": "**/vs/base/node/**", + "restrictions": [ + "vs/nls", + "**/vs/base/{common,node}/**", + "*" // node modules + ] + }, + { + // vs/base/test/browser contains tests for vs/base/browser + "target": "**/vs/base/test/browser/**", + "restrictions": [ + "assert", + "sinon", + "vs/nls", + "**/vs/base/{common,browser}/**", + "**/vs/base/test/{common,browser}/**" + ] + }, + { + "target": "**/vs/base/parts/*/common/**", + "restrictions": [ + "vs/nls", + "**/vs/base/common/**", + "**/vs/base/parts/*/common/**" + ] + }, + { + "target": "**/vs/base/parts/*/browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/{common,browser}/**", + "**/vs/base/parts/*/{common,browser}/**" + ] + }, + { + "target": "**/vs/base/parts/*/node/**", + "restrictions": [ + "vs/nls", + "**/vs/base/{common,node}/**", + "**/vs/base/parts/*/{common,node}/**", + "*" // node modules + ] + }, + { + "target": "**/vs/base/parts/*/electron-sandbox/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/{common,browser,electron-sandbox}/**", + "**/vs/base/parts/*/{common,browser,electron-sandbox}/**" + ] + }, + { + "target": "**/vs/base/parts/*/electron-browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/base/parts/*/{common,browser,node,electron-sandbox,electron-browser}/**", + "*" // node modules + ] + }, + { + "target": "**/vs/base/parts/*/electron-main/**", + "restrictions": [ + "vs/nls", + "**/vs/base/{common,node,electron-main}/**", + "**/vs/base/parts/*/{common,node,electron-main}/**", + "*" // node modules + ] + }, + { + "target": "**/vs/platform/*/common/**", + "restrictions": [ + "vs/nls", + "**/vs/base/common/**", + "**/vs/base/parts/*/common/**", + "**/vs/platform/*/common/**" + ] + }, + { + "target": "**/vs/platform/*/test/common/**", + "restrictions": [ + "assert", + "sinon", + "vs/nls", + "**/vs/base/common/**", + "**/vs/base/parts/*/common/**", + "**/vs/base/test/common/**", + "**/vs/platform/*/common/**", + "**/vs/platform/*/test/common/**" + ] + }, + { + "target": "**/vs/platform/*/browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/{common,browser}/**", + "**/vs/base/parts/*/{common,browser}/**", + "**/vs/platform/*/{common,browser}/**" + ] + }, + { + "target": "**/vs/platform/*/node/**", + "restrictions": [ + "vs/nls", + "**/vs/base/{common,node}/**", + "**/vs/base/parts/*/{common,node}/**", + "**/vs/platform/*/{common,node}/**", + "*" // node modules + ] + }, + { + "target": "**/vs/platform/*/electron-sandbox/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/{common,browser,electron-sandbox}/**", + "**/vs/base/parts/*/{common,browser,electron-sandbox}/**", + "**/vs/platform/*/{common,browser,electron-sandbox}/**" + ] + }, + { + "target": "**/vs/platform/*/electron-browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/base/parts/*/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/platform/*/{common,browser,node,electron-sandbox,electron-browser}/**", + "*" // node modules + ] + }, + { + "target": "**/vs/platform/*/electron-main/**", + "restrictions": [ + "vs/nls", + "**/vs/base/{common,node,electron-main}/**", + "**/vs/base/parts/*/{common,node,electron-main}/**", + "**/vs/platform/*/{common,node,electron-main}/**", + "**/vs/code/**", + "*" // node modules + ] + }, + { + "target": "**/vs/platform/*/test/browser/**", + "restrictions": [ + "assert", + "sinon", + "vs/nls", + "**/vs/base/{common,browser}/**", + "**/vs/platform/*/{common,browser}/**", + "**/vs/platform/*/test/{common,browser}/**" + ] + }, + { + "target": "**/vs/editor/common/**", + "restrictions": [ + "vs/nls", + "**/vs/base/common/**", + "**/vs/base/worker/**", + "**/vs/platform/*/common/**", + "**/vs/editor/common/**" + ] + }, + { + "target": "**/vs/editor/test/common/**", + "restrictions": [ + "assert", + "sinon", + "vs/nls", + "**/vs/base/common/**", + "**/vs/platform/*/common/**", + "**/vs/platform/*/test/common/**", + "**/vs/editor/common/**", + "**/vs/editor/test/common/**" + ] + }, + { + "target": "**/vs/editor/browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/{common,browser}/**", + "**/vs/platform/*/{common,browser}/**", + "**/vs/editor/{common,browser}/**" + ] + }, + { + "target": "**/vs/editor/test/browser/**", + "restrictions": [ + "assert", + "sinon", + "vs/nls", + "**/vs/base/{common,browser}/**", + "**/vs/platform/*/{common,browser}/**", + "**/vs/platform/*/test/{common,browser}/**", + "**/vs/editor/{common,browser}/**", + "**/vs/editor/test/{common,browser}/**" + ] + }, + { + "target": "**/vs/editor/standalone/common/**", + "restrictions": [ + "vs/nls", + "**/vs/base/common/**", + "**/vs/platform/*/common/**", + "**/vs/editor/common/**", + "**/vs/editor/standalone/common/**" + ] + }, + { + "target": "**/vs/editor/standalone/test/common/**", + "restrictions": [ + "assert", + "sinon", + "vs/nls", + "**/vs/base/common/**", + "**/vs/platform/*/common/**", + "**/vs/platform/*/test/common/**", + "**/vs/editor/common/**", + "**/vs/editor/test/common/**" + ] + }, + { + "target": "**/vs/editor/standalone/browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/{common,browser}/**", + "**/vs/base/parts/*/{common,browser}/**", + "**/vs/platform/*/{common,browser}/**", + "**/vs/editor/{common,browser}/**", + "**/vs/editor/contrib/**", + "**/vs/editor/standalone/{common,browser}/**" + ] + }, + { + "target": "**/vs/editor/standalone/test/browser/**", + "restrictions": [ + "assert", + "sinon", + "vs/nls", + "**/vs/base/{common,browser}/**", + "**/vs/platform/*/{common,browser}/**", + "**/vs/platform/*/test/{common,browser}/**", + "**/vs/editor/{common,browser}/**", + "**/vs/editor/standalone/{common,browser}/**", + "**/vs/editor/test/{common,browser}/**" + ] + }, + { + "target": "**/vs/editor/contrib/*/test/**", + "restrictions": [ + "assert", + "sinon", + "vs/nls", + "**/vs/base/{common,browser}/**", + "**/vs/base/test/{common,browser}/**", + "**/vs/base/parts/*/{common,browser}/**", + "**/vs/platform/*/{common,browser}/**", + "**/vs/platform/*/test/{common,browser}/**", + "**/vs/editor/{common,browser}/**", + "**/vs/editor/test/{common,browser}/**", + "**/vs/editor/contrib/**" + ] + }, + { + "target": "**/vs/editor/contrib/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/{common,browser}/**", + "**/vs/base/parts/*/{common,browser}/**", + "**/vs/platform/{common,browser}/**", + "**/vs/platform/*/{common,browser}/**", + "**/vs/editor/{common,browser}/**", + "**/vs/editor/contrib/**" + ] + }, + { + "target": "**/vs/workbench/common/**", + "restrictions": [ + "vs/nls", + "**/vs/base/common/**", + "**/vs/base/parts/*/common/**", + "**/vs/platform/*/common/**", + "**/vs/editor/common/**", + "**/vs/editor/contrib/*/common/**", + "**/vs/workbench/common/**", + "**/vs/workbench/services/*/common/**", + "assert" + ] + }, + { + "target": "**/vs/workbench/browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/{common,browser}/**", + "**/vs/base/parts/*/{common,browser}/**", + "**/vs/platform/*/{common,browser}/**", + "**/vs/editor/{common,browser}/**", + "**/vs/editor/contrib/**", // editor/contrib is equivalent to /browser/ by convention + "**/vs/workbench/workbench.web.api", + "**/vs/workbench/{common,browser}/**", + "**/vs/workbench/services/*/{common,browser}/**", + "assert" + ] + }, + { + "target": "**/vs/workbench/api/common/**", + "restrictions": [ + "vscode", + "vs/nls", + "**/vs/base/common/**", + "**/vs/platform/*/common/**", + "**/vs/editor/common/**", + "**/vs/editor/contrib/*/common/**", + "**/vs/workbench/api/common/**", + "**/vs/workbench/common/**", + "**/vs/workbench/services/*/common/**", + "**/vs/workbench/contrib/*/common/**" + ] + }, + { + "target": "**/vs/workbench/api/worker/**", + "restrictions": [ + "vscode", + "vs/nls", + "**/vs/**/{common,worker}/**" + ] + }, + { + "target": "**/vs/workbench/electron-sandbox/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/{common,browser,electron-sandbox}/**", + "**/vs/base/parts/*/{common,browser,electron-sandbox}/**", + "**/vs/platform/*/{common,browser,electron-sandbox}/**", + "**/vs/editor/{common,browser,electron-sandbox}/**", + "**/vs/editor/contrib/**", // editor/contrib is equivalent to /browser/ by convention + "**/vs/workbench/{common,browser,electron-sandbox}/**", + "**/vs/workbench/api/{common,browser,electron-sandbox}/**", + "**/vs/workbench/services/*/{common,browser,electron-sandbox}/**" + ] + }, + { + "target": "**/vs/workbench/electron-browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/base/parts/*/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/platform/*/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/editor/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/editor/contrib/**", // editor/contrib is equivalent to /browser/ by convention + "**/vs/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/workbench/api/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/workbench/services/*/{common,browser,node,electron-sandbox,electron-browser}/**", + "*" // node modules + ] + }, + { + "target": "**/vs/workbench/services/**/test/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**", + "**/vs/platform/**", + "**/vs/editor/**", + "**/vs/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", + "vs/workbench/contrib/files/common/editors/fileEditorInput", + "**/vs/workbench/services/**", + "**/vs/workbench/test/**", + "*" // node modules + ] + }, + { + "target": "**/vs/workbench/services/**/common/**", + "restrictions": [ + "vs/nls", + "**/vs/base/**/common/**", + "**/vs/platform/**/common/**", + "**/vs/editor/common/**", + "**/vs/workbench/workbench.web.api", + "**/vs/workbench/common/**", + "**/vs/workbench/services/**/common/**", + "**/vs/workbench/api/**/common/**", + "vscode-textmate", + "vscode-oniguruma", + "iconv-lite-umd", + "tas-client-umd", + "jschardet" + ] + }, + { + "target": "**/vs/workbench/services/**/worker/**", + "restrictions": [ + "vs/nls", + "**/vs/base/**/common/**", + "**/vs/platform/**/common/**", + "**/vs/editor/common/**", + "**/vs/workbench/**/common/**", + "**/vs/workbench/**/worker/**", + "**/vs/workbench/services/**/common/**", + "vscode" + ] + }, + { + "target": "**/vs/workbench/services/**/browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**/{common,browser,worker}/**", + "**/vs/platform/**/{common,browser}/**", + "**/vs/editor/{common,browser}/**", + "**/vs/workbench/workbench.web.api", + "**/vs/workbench/{common,browser}/**", + "**/vs/workbench/api/{common,browser}/**", + "**/vs/workbench/services/**/{common,browser}/**", + "vscode-textmate", + "vscode-oniguruma", + "iconv-lite-umd", + "jschardet" + ] + }, + { + "target": "**/vs/workbench/services/**/node/**", + "restrictions": [ + "vs/nls", + "**/vs/base/**/{common,node}/**", + "**/vs/platform/**/{common,node}/**", + "**/vs/editor/{common,node}/**", + "**/vs/workbench/{common,node}/**", + "**/vs/workbench/api/{common,node}/**", + "**/vs/workbench/services/**/{common,node}/**", + "*" // node modules + ] + }, + { + "target": "**/vs/workbench/services/**/electron-sandbox/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**/{common,browser,worker,electron-sandbox}/**", + "**/vs/platform/**/{common,browser,electron-sandbox}/**", + "**/vs/editor/**", + "**/vs/workbench/{common,browser,electron-sandbox}/**", + "**/vs/workbench/api/{common,browser,electron-sandbox}/**", + "**/vs/workbench/services/**/{common,browser,electron-sandbox}/**", + "vscode-textmate", + "vscode-oniguruma", + "iconv-lite-umd", + "jschardet" + ] + }, + { + "target": "**/vs/workbench/services/**/electron-browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**/{common,browser,worker,node,electron-sandbox,electron-browser}/**", + "**/vs/platform/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/editor/**", + "**/vs/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/workbench/api/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/workbench/services/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "*" // node modules + ] + }, + { + "target": "**/vs/workbench/contrib/**/test/**", + "restrictions": [ + "assert", + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**", + "**/vs/platform/**", + "**/vs/editor/**", + "**/vs/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/workbench/services/**", + "**/vs/workbench/contrib/**", + "**/vs/workbench/test/**", + "*" + ] + }, + { + "target": "**/vs/workbench/contrib/terminal/browser/**", + "restrictions": [ + // xterm and its addons are strictly browser-only components + "xterm", + "xterm-addon-*", + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**/{common,browser}/**", + "**/vs/platform/**/{common,browser}/**", + "**/vs/editor/**", + "**/vs/workbench/{common,browser}/**", + "**/vs/workbench/contrib/**/{common,browser}/**", + "**/vs/workbench/services/**/{common,browser}/**" + ] + }, + { + "target": "**/vs/workbench/contrib/extensions/browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**/{common,browser}/**", + "**/vs/platform/**/{common,browser}/**", + "**/vs/editor/**", + "**/vs/workbench/{common,browser}/**", + "**/vs/workbench/contrib/**/{common,browser}/**", + "**/vs/workbench/services/**/{common,browser}/**" + ] + }, + { + "target": "**/vs/workbench/contrib/update/browser/update.ts", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**/{common,browser}/**", + "**/vs/platform/**/{common,browser}/**", + "**/vs/editor/**", + "**/vs/workbench/{common,browser}/**", + "**/vs/workbench/contrib/**/{common,browser}/**", + "**/vs/workbench/services/**/{common,browser}/**" + ] + }, + { + "target": "**/vs/workbench/contrib/notebook/common/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**/{common,worker}/**", + "**/vs/platform/**/common/**", + "**/vs/editor/**", + "**/vs/workbench/common/**", + "**/vs/workbench/api/common/**", + "**/vs/workbench/services/**/common/**", + "**/vs/workbench/contrib/**/common/**" + ] + }, + { + "target": "**/vs/workbench/contrib/**/common/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**/common/**", + "**/vs/platform/**/common/**", + "**/vs/editor/**", + "**/vs/workbench/common/**", + "**/vs/workbench/api/common/**", + "**/vs/workbench/services/**/common/**", + "**/vs/workbench/contrib/**/common/**" + ] + }, + { + "target": "**/vs/workbench/contrib/**/browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**/{common,browser}/**", + "**/vs/platform/**/{common,browser}/**", + "**/vs/editor/**", + "**/vs/workbench/{common,browser}/**", + "**/vs/workbench/api/{common,browser}/**", + "**/vs/workbench/services/**/{common,browser}/**", + "**/vs/workbench/contrib/**/{common,browser}/**", + "vscode-textmate", + "vscode-oniguruma", + "iconv-lite-umd", + "jschardet" + ] + }, + { + "target": "**/vs/workbench/contrib/**/node/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**/{common,node}/**", + "**/vs/platform/**/{common,node}/**", + "**/vs/editor/**/common/**", + "**/vs/workbench/{common,node}/**", + "**/vs/workbench/api/{common,node}/**", + "**/vs/workbench/services/**/{common,node}/**", + "**/vs/workbench/contrib/**/{common,node}/**", + "*" // node modules + ] + }, + { + "target": "**/vs/workbench/contrib/**/electron-sandbox/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**/{common,browser,worker,electron-sandbox}/**", + "**/vs/platform/**/{common,browser,electron-sandbox}/**", + "**/vs/editor/**", + "**/vs/workbench/{common,browser,electron-sandbox}/**", + "**/vs/workbench/api/{common,browser,electron-sandbox}/**", + "**/vs/workbench/services/**/{common,browser,electron-sandbox}/**", + "**/vs/workbench/contrib/**/{common,browser,electron-sandbox}/**", + "vscode-textmate", + "vscode-oniguruma", + "iconv-lite-umd", + "jschardet" + ] + }, + { + "target": "**/vs/workbench/contrib/**/electron-browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**/{common,browser,worker,node,electron-sandbox,electron-browser}/**", + "**/vs/platform/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/editor/**", + "**/vs/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/workbench/api/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/workbench/services/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/workbench/contrib/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "*" // node modules + ] + }, + { + "target": "**/vs/code/browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**/{common,browser}/**", + "**/vs/base/parts/**/{common,browser}/**", + "**/vs/platform/**/{common,browser}/**", + "**/vs/code/**/{common,browser}/**", + "**/vs/workbench/workbench.web.api" + ] + }, + { + "target": "**/vs/code/node/**", + "restrictions": [ + "vs/nls", + "**/vs/base/**/{common,node}/**", + "**/vs/base/parts/**/{common,node}/**", + "**/vs/platform/**/{common,node}/**", + "**/vs/code/**/{common,node}/**", + "*" // node modules + ] + }, + { + "target": "**/vs/code/electron-browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/base/parts/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/platform/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/code/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "*" // node modules + ] + }, + { + "target": "**/vs/code/electron-main/**", + "restrictions": [ + "vs/nls", + "**/vs/base/**/{common,node,electron-main}/**", + "**/vs/base/parts/**/{common,node,electron-main}/**", + "**/vs/platform/**/{common,node,electron-main}/**", + "**/vs/code/**/{common,node,electron-main}/**", + "*" // node modules + ] + }, + { + "target": "**/vs/server/**", + "restrictions": [ + "vs/nls", + "**/vs/base/**/{common,node}/**", + "**/vs/base/parts/**/{common,node}/**", + "**/vs/platform/**/{common,node}/**", + "**/vs/workbench/**/{common,node}/**", + "**/vs/server/**", + "**/vs/code/**/{common,node}/**", + "*" // node modules + ] + }, + { + "target": "**/src/vs/workbench/workbench.common.main.ts", + "restrictions": [ + "vs/nls", + "**/vs/base/**/{common,browser}/**", + "**/vs/base/parts/**/{common,browser}/**", + "**/vs/platform/**/{common,browser}/**", + "**/vs/editor/**", + "**/vs/workbench/**/{common,browser}/**" + ] + }, + { + "target": "**/src/vs/workbench/workbench.web.main.ts", + "restrictions": [ + "vs/nls", + "**/vs/base/**/{common,browser}/**", + "**/vs/base/parts/**/{common,browser}/**", + "**/vs/platform/**/{common,browser}/**", + "**/vs/editor/**", + "**/vs/workbench/**/{common,browser}/**", + "**/vs/workbench/workbench.common.main" + ] + }, + { + "target": "**/src/vs/workbench/workbench.web.api.ts", + "restrictions": [ + "vs/nls", + "**/vs/base/**/{common,browser}/**", + "**/vs/base/parts/**/{common,browser}/**", + "**/vs/platform/**/{common,browser}/**", + "**/vs/editor/**", + "**/vs/workbench/**/{common,browser}/**", + "**/vs/workbench/workbench.web.main" + ] + }, + { + "target": "**/src/vs/workbench/workbench.sandbox.main.ts", + "restrictions": [ + "vs/nls", + "**/vs/base/**/{common,browser,electron-sandbox}/**", + "**/vs/base/parts/**/{common,browser,electron-sandbox}/**", + "**/vs/platform/**/{common,browser,electron-sandbox}/**", + "**/vs/editor/**", + "**/vs/workbench/**/{common,browser,electron-sandbox}/**", + "**/vs/workbench/workbench.common.main" + ] + }, + { + "target": "**/src/vs/workbench/workbench.desktop.main.ts", + "restrictions": [ + "vs/nls", + "**/vs/base/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/base/parts/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/platform/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/editor/**", + "**/vs/workbench/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/workbench/workbench.common.main", + "**/vs/workbench/workbench.sandbox.main" + ] + }, + { + "target": "**/extensions/**", + "restrictions": "**/*" + }, + { + "target": "**/test/smoke/**", + "restrictions": [ + "**/test/smoke/**", + "*" // node modules + ] + }, + { + "target": "**/test/automation/**", + "restrictions": [ + "**/test/automation/**", + "*" // node modules + ] + }, + { + "target": "**/test/integration/**", + "restrictions": [ + "**/test/integration/**", + "*" // node modules + ] + }, + { + "target": "**/test/monaco/**", + "restrictions": [ + "**/test/monaco/**", + "*" // node modules + ] + }, + { + "target": "**/api/**.test.ts", + "restrictions": [ + "**/vs/**", + "assert", + "sinon", + "crypto", + "vscode" + ] + }, + { + "target": "**/{node,electron-browser,electron-main}/**/*.test.ts", + "restrictions": [ + "**/vs/**", + "*" // node modules + ] + }, + { + "target": "**/{node,electron-browser,electron-main}/**/test/**", + "restrictions": [ + "**/vs/**", + "*" // node modules + ] + }, + { + "target": "**/test/{node,electron-browser,electron-main}/**", + "restrictions": [ + "**/vs/**", + "*" // node modules + ] + }, + { + "target": "**/**.test.ts", + "restrictions": [ + "**/vs/**", + "assert", + "sinon", + "crypto", + "xterm*" + ] + }, + { + "target": "**/test/**", + "restrictions": [ + "**/vs/**", + "assert", + "sinon", + "crypto", + "xterm*" + ] + } + ] + }, + "overrides": [ + { + "files": [ + "*.js" + ], + "rules": { + "jsdoc/no-types": "off" + } + }, + { + "files": [ + "**/vscode.d.ts", + "**/vscode.proposed.d.ts" + ], + "rules": { + "vscode-dts-create-func": "warn", + "vscode-dts-literal-or-types": "warn", + "vscode-dts-interface-naming": "warn", + "vscode-dts-cancellation": "warn", + "vscode-dts-use-thenable": "warn", + "vscode-dts-region-comments": "warn", + "vscode-dts-provider-naming": [ + "warn", + { + "allowed": [ + "FileSystemProvider", + "TreeDataProvider", + "CustomEditorProvider", + "CustomReadonlyEditorProvider", + "TerminalLinkProvider" + ] + } + ], + "vscode-dts-event-naming": [ + "warn", + { + "allowed": [ + "onCancellationRequested", + "event" + ], + "verbs": [ + "accept", + "change", + "close", + "collapse", + "create", + "delete", + "discover", + "dispose", + "edit", + "end", + "expand", + "hide", + "open", + "override", + "receive", + "register", + "rename", + "save", + "send", + "start", + "terminate", + "trigger", + "unregister", + "write" + ] + } + ] + } + } + ] } diff --git a/.github/actions/build-chat/.gitignore b/.github/actions/build-chat/.gitignore new file mode 100644 index 00000000..db4c6d9b --- /dev/null +++ b/.github/actions/build-chat/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules \ No newline at end of file diff --git a/.github/actions/build-chat/action.yml b/.github/actions/build-chat/action.yml new file mode 100644 index 00000000..278475c9 --- /dev/null +++ b/.github/actions/build-chat/action.yml @@ -0,0 +1,10 @@ +name: 'Build Chat' +description: 'Notify in chat about build results.' +author: 'Christof Marti' +inputs: + workflow_run_url: + description: 'Workflow run URL of the completed build.' + required: true +runs: + using: 'node12' + main: 'dist/main.js' diff --git a/.github/actions/build-chat/package.json b/.github/actions/build-chat/package.json new file mode 100644 index 00000000..156717a8 --- /dev/null +++ b/.github/actions/build-chat/package.json @@ -0,0 +1,23 @@ +{ + "name": "build-chat", + "version": "0.0.0", + "author": "Microsoft Corporation", + "license": "MIT", + "description": "A GitHub action to create a Windows Package Manager manifest file.", + "main": "dist/main.js", + "scripts": { + "build": "tsc" + }, + "dependencies": { + "@actions/core": "^1.2.6", + "@octokit/rest": "^18.0.12", + "@slack/web-api": "^6.0.0", + "azure-storage": "^2.10.3", + "stream-buffers": "^3.0.2" + }, + "devDependencies": { + "@types/node": "^14.14.22", + "@types/stream-buffers": "^3.0.3", + "typescript": "^4.1.3" + } +} diff --git a/.github/actions/build-chat/src/main.ts b/.github/actions/build-chat/src/main.ts new file mode 100644 index 00000000..71e03585 --- /dev/null +++ b/.github/actions/build-chat/src/main.ts @@ -0,0 +1,217 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as core from '@actions/core'; +import { Octokit, RestEndpointMethodTypes } from '@octokit/rest'; +import { WebClient } from '@slack/web-api'; +import * as storage from 'azure-storage'; +import { WritableStreamBuffer } from 'stream-buffers'; + +(async () => { + const actionUrl = core.getInput('workflow_run_url'); + const url = actionUrl || 'https://api.github.com/repos/microsoft/vscode/actions/runs/503514090'; + console.log(url); + const parts = url.split('/'); + const owner = parts[parts.length - 5]; + const repo = parts[parts.length - 4]; + const runId = parseInt(parts[parts.length - 1], 10); + if (actionUrl) { + await handleNotification(owner, repo, runId); + } else { + const results = await buildComplete(owner, repo, runId); + for (const message of [...results.logMessages, ...results.messages]) { + console.log(message); + } + } +})() + .then(null, console.error); + +const testChannels = ['bot-log', 'bot-test-log']; + +async function handleNotification(owner: string, repo: string, runId: number) { + + const results = await buildComplete(owner, repo, runId); + if (results.logMessages.length || results.messages.length) { + + const web = new WebClient(process.env.SLACK_TOKEN); + const memberships = await listAllMemberships(web); + const memberTestChannels = memberships.filter(m => testChannels.indexOf(m.name) !== -1); + + for (const message of results.logMessages) { + for (const testChannel of memberTestChannels) { + await web.chat.postMessage({ + text: message, + link_names: true, + channel: testChannel.id, + }); + } + } + for (const message of results.messages) { + for (const channel of memberships) { + await web.chat.postMessage({ + text: message, + link_names: true, + channel: channel.id, + }); + } + } + } +} + +async function buildComplete(owner: string, repo: string, runId: number) { + console.log(`buildComplete: https://github.com/${owner}/${repo}/actions/runs/${runId}`); + const auth = `token ${process.env.GITHUB_TOKEN}`; + const octokit = new Octokit({ auth }); + const buildResult = (await octokit.actions.getWorkflowRun({ + owner, + repo, + run_id: runId, + })).data; + if (buildResult.head_branch !== 'master' && !buildResult.head_branch?.startsWith('release/')) { + console.error('Private branch. Terminating.') + return { logMessages: [], messages: [] }; + } + + // const buildQuery = `${buildsApiUrl}?$top=10&maxTime=${buildResult.finishTime}&definitions=${buildResult.definition.id}&branchName=${buildResult.sourceBranch}&resultFilter=${results.join(',')}&api-version=5.0-preview.4`; + + const buildResults = (await octokit.actions.listWorkflowRuns({ + owner, + repo, + workflow_id: buildResult.workflow_id, + branch: buildResult.head_branch || undefined, + per_page: 5, // More returns 502s. + })).data.workflow_runs + .filter(run => run.status === 'completed'); + + const currentBuildIndex = buildResults.findIndex(build => build.id === buildResult.id); + if (currentBuildIndex === -1) { + console.error('Build not on first page. Terminating.') + console.error(buildResults.map(({ id, status, conclusion }) => ({ id, status, conclusion }))); + return { logMessages: [], messages: [] }; + } + const slicedResults = buildResults.slice(currentBuildIndex, currentBuildIndex + 2); + const builds = slicedResults + .map((build, i, array) => ({ + data: build, + previousSourceVersion: i < array.length - 1 ? array[i + 1].head_sha : undefined, + authors: [], + buildHtmlUrl: build.html_url, + changesHtmlUrl: '', + })); + const logMessages = builds.slice(0, 1) + .map(build => `Id: ${build.data.id} | Branch: ${build.data.head_branch} | Conclusion: ${build.data.conclusion} | Created: ${build.data.created_at} | Updated: ${build.data.updated_at}`); + const transitionedBuilds = builds.filter((build, i, array) => i < array.length - 1 && transitioned(build, array[i + 1])); + await Promise.all(transitionedBuilds + .map(async build => { + if (build.previousSourceVersion) { + const cmp = await compareCommits(octokit, owner, repo, build.previousSourceVersion, build.data.head_sha); + const commits = cmp.data.commits; + const authors = new Set([ + ...commits.map((c: any) => c.author.login), + ...commits.map((c: any) => c.committer.login), + ]); + authors.delete('web-flow'); // GitHub Web UI committer + build.authors = [...authors]; + build.changesHtmlUrl = `https://github.com/${owner}/${repo}/compare/${build.previousSourceVersion.substr(0, 7)}...${build.data.head_sha.substr(0, 7)}`; // Shorter than: cmp.data.html_url + } + })); + const vscode = repo === 'vscode'; + const name = vscode ? `VS Code ${buildResult.name} Build` : buildResult.name; + // TBD: `Requester: ${vstsToSlackUser(build.requester, build.degraded)}${pingBenForSmokeTests && releaseBuild && build.result === 'partiallySucceeded' ? ' | Ping: @bpasero' : ''}` + const accounts = await readAccounts(); + const githubAccountMap = githubToAccounts(accounts); + const messages = transitionedBuilds.map(build => `${name} +Result: ${build.data.conclusion} | Branch: ${build.data.head_branch} | Authors: ${githubToSlackUsers(githubAccountMap, build.authors, build.degraded).sort().join(', ') || `None (rebuild)`} +Build: ${build.buildHtmlUrl} +Changes: ${build.changesHtmlUrl}`); + return { logMessages, messages }; +} + +const conclusions = ['success', 'failure'] + +function transitioned(newer: Build, older: Build) { + const newerResult = newer.data.conclusion || 'success'; + const olderResult = older.data.conclusion || 'success'; + if (newerResult === olderResult) { + return false; + } + if (conclusions.indexOf(newerResult) > conclusions.indexOf(olderResult)) { + newer.degraded = true; + } + return true; +} + +async function compareCommits(octokit: Octokit, owner: string, repo: string, base: string, head: string) { + return octokit.repos.compareCommits({ owner, repo, base, head }); +} + +function githubToSlackUsers(githubToAccounts: Record, githubUsers: string[], at?: boolean) { + return githubUsers.map(g => githubToAccounts[g] ? `${at ? '@' : ''}${githubToAccounts[g].slack}` : g); +} + +interface Accounts { + github: string; + slack: string; + vsts: string; +} + +function githubToAccounts(accounts: Accounts[]) { + return accounts.reduce((m, e) => { + m[e.github] = e; + return m; + }, >{}); +} + +async function readAccounts() { + const connectionString = process.env.BUILD_CHAT_STORAGE_CONNECTION_STRING; + if (!connectionString) { + console.error('Connection string missing.'); + return []; + } + const buf = await readFile(connectionString, 'config', '/', 'accounts.json'); + return JSON.parse(buf.toString()) as Accounts[]; +} + +async function readFile(connectionString: string, share: string, directory: string, filename: string) { + return new Promise((resolve, reject) => { + const stream = new WritableStreamBuffer() + const fileService = storage.createFileService(connectionString); + fileService.getFileToStream(share, directory, filename, stream, err => { + if (err) { + reject(err); + } else { + const contents = stream.getContents(); + if (contents) { + resolve(contents); + } else { + reject(new Error('No content')); + } + } + }); + }); +} + +interface AllChannels { + channels: { + id: string; + name: string; + is_member: boolean; + }[]; +} + +async function listAllMemberships(web: WebClient) { + const groups = await web.conversations.list({ types: 'public_channel,private_channel' }) as unknown as AllChannels; + return groups.channels + .filter(c => c.is_member); +} + +interface Build { + data: RestEndpointMethodTypes['actions']['getWorkflowRun']['response']['data']; + previousSourceVersion: string | undefined; + authors: string[]; + buildHtmlUrl: string; + changesHtmlUrl: string; + degraded?: boolean; +} diff --git a/.github/actions/build-chat/tsconfig.json b/.github/actions/build-chat/tsconfig.json new file mode 100644 index 00000000..f4889aab --- /dev/null +++ b/.github/actions/build-chat/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2017", + "strict": true, + "noUnusedLocals": true, + "resolveJsonModule": true, + "lib": [ + "es2017" + ], + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + }, + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/.github/actions/build-chat/yarn.lock b/.github/actions/build-chat/yarn.lock new file mode 100644 index 00000000..bc218523 --- /dev/null +++ b/.github/actions/build-chat/yarn.lock @@ -0,0 +1,728 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@actions/core@^1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.2.6.tgz#a78d49f41a4def18e88ce47c2cac615d5694bf09" + integrity sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA== + +"@octokit/auth-token@^2.4.4": + version "2.4.4" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.4.tgz#ee31c69b01d0378c12fd3ffe406030f3d94d3b56" + integrity sha512-LNfGu3Ro9uFAYh10MUZVaT7X2CnNm2C8IDQmabx+3DygYIQjs9FwzFAHN/0t6mu5HEPhxcb1XOuxdpY82vCg2Q== + dependencies: + "@octokit/types" "^6.0.0" + +"@octokit/core@^3.2.3": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.2.4.tgz#5791256057a962eca972e31818f02454897fd106" + integrity sha512-d9dTsqdePBqOn7aGkyRFe7pQpCXdibSJ5SFnrTr0axevObZrpz3qkWm7t/NjYv5a66z6vhfteriaq4FRz3e0Qg== + dependencies: + "@octokit/auth-token" "^2.4.4" + "@octokit/graphql" "^4.5.8" + "@octokit/request" "^5.4.12" + "@octokit/types" "^6.0.3" + before-after-hook "^2.1.0" + universal-user-agent "^6.0.0" + +"@octokit/endpoint@^6.0.1": + version "6.0.10" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.10.tgz#741ce1fa2f4fb77ce8ebe0c6eaf5ce63f565f8e8" + integrity sha512-9+Xef8nT7OKZglfkOMm7IL6VwxXUQyR7DUSU0LH/F7VNqs8vyd7es5pTfz9E7DwUIx7R3pGscxu1EBhYljyu7Q== + dependencies: + "@octokit/types" "^6.0.0" + is-plain-object "^5.0.0" + universal-user-agent "^6.0.0" + +"@octokit/graphql@^4.5.8": + version "4.5.8" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.5.8.tgz#d42373633c3015d0eafce64a8ce196be167fdd9b" + integrity sha512-WnCtNXWOrupfPJgXe+vSmprZJUr0VIu14G58PMlkWGj3cH+KLZEfKMmbUQ6C3Wwx6fdhzVW1CD5RTnBdUHxhhA== + dependencies: + "@octokit/request" "^5.3.0" + "@octokit/types" "^6.0.0" + universal-user-agent "^6.0.0" + +"@octokit/openapi-types@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-3.0.0.tgz#f73d48af2d21bf4f97fbf38fae43b54699e0dbba" + integrity sha512-jOp1CVRw+OBJaZtG9QzZggvJXvyzgDXuW948SWsDiwmyDuCjeYCiF3TDD/qvhpF580RfP7iBIos4AVU6yhgMlA== + +"@octokit/plugin-paginate-rest@^2.6.2": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.8.0.tgz#2b41e12b494e895bf5fb5b12565d2c80a0ecc6ae" + integrity sha512-HtuEQ2AYE4YFEBQN0iHmMsIvVucd5RsnwJmRKIsfAg1/ZeoMaU+jXMnTAZqIUEmcVJA27LjHUm3f1hxf8Fpdxw== + dependencies: + "@octokit/types" "^6.4.0" + +"@octokit/plugin-request-log@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.2.tgz#394d59ec734cd2f122431fbaf05099861ece3c44" + integrity sha512-oTJSNAmBqyDR41uSMunLQKMX0jmEXbwD1fpz8FG27lScV3RhtGfBa1/BBLym+PxcC16IBlF7KH9vP1BUYxA+Eg== + +"@octokit/plugin-rest-endpoint-methods@4.4.1": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.4.1.tgz#105cf93255432155de078c9efc33bd4e14d1cd63" + integrity sha512-+v5PcvrUcDeFXf8hv1gnNvNLdm4C0+2EiuWt9EatjjUmfriM1pTMM+r4j1lLHxeBQ9bVDmbywb11e3KjuavieA== + dependencies: + "@octokit/types" "^6.1.0" + deprecation "^2.3.1" + +"@octokit/request-error@^2.0.0": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.0.4.tgz#07dd5c0521d2ee975201274c472a127917741262" + integrity sha512-LjkSiTbsxIErBiRh5wSZvpZqT4t0/c9+4dOe0PII+6jXR+oj/h66s7E4a/MghV7iT8W9ffoQ5Skoxzs96+gBPA== + dependencies: + "@octokit/types" "^6.0.0" + deprecation "^2.0.0" + once "^1.4.0" + +"@octokit/request@^5.3.0", "@octokit/request@^5.4.12": + version "5.4.12" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.4.12.tgz#b04826fa934670c56b135a81447be2c1723a2ffc" + integrity sha512-MvWYdxengUWTGFpfpefBBpVmmEYfkwMoxonIB3sUGp5rhdgwjXL1ejo6JbgzG/QD9B/NYt/9cJX1pxXeSIUCkg== + dependencies: + "@octokit/endpoint" "^6.0.1" + "@octokit/request-error" "^2.0.0" + "@octokit/types" "^6.0.3" + deprecation "^2.0.0" + is-plain-object "^5.0.0" + node-fetch "^2.6.1" + once "^1.4.0" + universal-user-agent "^6.0.0" + +"@octokit/rest@^18.0.12": + version "18.0.12" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.0.12.tgz#278bd41358c56d87c201e787e8adc0cac132503a" + integrity sha512-hNRCZfKPpeaIjOVuNJzkEL6zacfZlBPV8vw8ReNeyUkVvbuCvvrrx8K8Gw2eyHHsmd4dPlAxIXIZ9oHhJfkJpw== + dependencies: + "@octokit/core" "^3.2.3" + "@octokit/plugin-paginate-rest" "^2.6.2" + "@octokit/plugin-request-log" "^1.0.2" + "@octokit/plugin-rest-endpoint-methods" "4.4.1" + +"@octokit/types@^6.0.0", "@octokit/types@^6.0.3", "@octokit/types@^6.1.0", "@octokit/types@^6.4.0": + version "6.4.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.4.0.tgz#f3f47be70bcdb3c26f2c2619f3dd0ced466a265c" + integrity sha512-1FEmuVppZE2zG0rBdQlviRz5cp0udyI63zyhBVPrm0FRNAsQkAXU7IYWQg1XvhChFut8YbFYN1usQpk54D6/4w== + dependencies: + "@octokit/openapi-types" "^3.0.0" + "@types/node" ">= 8" + +"@slack/logger@>=1.0.0 <3.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@slack/logger/-/logger-2.0.0.tgz#6a4e1c755849bc0f66dac08a8be54ce790ec0e6b" + integrity sha512-OkIJpiU2fz6HOJujhlhfIGrc8hB4ibqtf7nnbJQDerG0BqwZCfmgtK5sWzZ0TkXVRBKD5MpLrTmCYyMxoMCgPw== + dependencies: + "@types/node" ">=8.9.0" + +"@slack/types@^1.7.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@slack/types/-/types-1.10.0.tgz#cbf7d83e1027f4cbfd13d6b429f120c7fb09127a" + integrity sha512-tA7GG7Tj479vojfV3AoxbckalA48aK6giGjNtgH6ihpLwTyHE3fIgRrvt8TWfLwW8X8dyu7vgmAsGLRG7hWWOg== + +"@slack/web-api@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@slack/web-api/-/web-api-6.0.0.tgz#14c65ed73c66a187e5f20e12c3898dfd8d5cbf7c" + integrity sha512-YD1wqWuzrYPf4RQyD7OnYS5lImUmNWn+G5V6Qt0N97fPYxqhT72YJtRdSnsTc3VkH5R5imKOhYxb+wqI9hiHnA== + dependencies: + "@slack/logger" ">=1.0.0 <3.0.0" + "@slack/types" "^1.7.0" + "@types/is-stream" "^1.1.0" + "@types/node" ">=12.0.0" + axios "^0.21.1" + eventemitter3 "^3.1.0" + form-data "^2.5.0" + is-stream "^1.1.0" + p-queue "^6.6.1" + p-retry "^4.0.0" + +"@types/is-stream@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/is-stream/-/is-stream-1.1.0.tgz#b84d7bb207a210f2af9bed431dc0fbe9c4143be1" + integrity sha512-jkZatu4QVbR60mpIzjINmtS1ZF4a/FqdTUTBeQDVOQ2PYyidtwFKr0B5G6ERukKwliq+7mIXvxyppwzG5EgRYg== + dependencies: + "@types/node" "*" + +"@types/node@*", "@types/node@>= 8", "@types/node@>=12.0.0", "@types/node@>=8.9.0", "@types/node@^14.14.22": + version "14.14.22" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.22.tgz#0d29f382472c4ccf3bd96ff0ce47daf5b7b84b18" + integrity sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw== + +"@types/retry@^0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== + +"@types/stream-buffers@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/stream-buffers/-/stream-buffers-3.0.3.tgz#34e565bf64e3e4bdeee23fd4aa58d4636014a02b" + integrity sha512-NeFeX7YfFZDYsCfbuaOmFQ0OjSmHreKBpp7MQ4alWQBHeh2USLsj7qyMyn9t82kjqIX516CR/5SRHnARduRtbQ== + dependencies: + "@types/node" "*" + +ajv@^6.12.3: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== + +axios@^0.21.1: + version "0.21.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" + integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== + dependencies: + follow-redirects "^1.10.0" + +azure-storage@^2.10.3: + version "2.10.3" + resolved "https://registry.yarnpkg.com/azure-storage/-/azure-storage-2.10.3.tgz#c5966bf929d87587d78f6847040ea9a4b1d4a50a" + integrity sha512-IGLs5Xj6kO8Ii90KerQrrwuJKexLgSwYC4oLWmc11mzKe7Jt2E5IVg+ZQ8K53YWZACtVTMBNO3iGuA+4ipjJxQ== + dependencies: + browserify-mime "~1.2.9" + extend "^3.0.2" + json-edm-parser "0.1.2" + md5.js "1.3.4" + readable-stream "~2.0.0" + request "^2.86.0" + underscore "~1.8.3" + uuid "^3.0.0" + validator "~9.4.1" + xml2js "0.2.8" + xmlbuilder "^9.0.7" + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +before-after-hook@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" + integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A== + +browserify-mime@~1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/browserify-mime/-/browserify-mime-1.2.9.tgz#aeb1af28de6c0d7a6a2ce40adb68ff18422af31f" + integrity sha1-rrGvKN5sDXpqLOQK22j/GEIq8x8= + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +combined-stream@^1.0.6, combined-stream@~1.0.6: + 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" + +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" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +deprecation@^2.0.0, deprecation@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" + integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +eventemitter3@^3.1.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" + integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== + +eventemitter3@^4.0.4: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +extend@^3.0.2, extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +follow-redirects@^1.10.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7" + integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg== + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" + integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.3: + version "5.1.5" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== + dependencies: + ajv "^6.12.3" + har-schema "^2.0.0" + +hash-base@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" + integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== + dependencies: + inherits "^2.0.4" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +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" + integrity sha1-HmCw/vG8CvZ7wNFG393lSGzWFbQ= + dependencies: + jsonparse "~1.2.0" + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +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= + +jsonparse@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.2.0.tgz#5c0c5685107160e72fe7489bddea0b44c2bc67bd" + integrity sha1-XAxWhRBxYOcv50ib3eoLRMK8Z70= + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +md5.js@1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" + integrity sha1-6b296UogpawYsENA/Fdk1bCdkB0= + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +mime-db@1.45.0: + version "1.45.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" + integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.28" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd" + integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ== + dependencies: + mime-db "1.45.0" + +node-fetch@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +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" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +p-queue@^6.6.1: + version "6.6.2" + resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" + integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== + dependencies: + eventemitter3 "^4.0.4" + p-timeout "^3.2.0" + +p-retry@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.2.0.tgz#ea9066c6b44f23cab4cd42f6147cdbbc6604da5d" + integrity sha512-jPH38/MRh263KKcq0wBNOGFJbm+U6784RilTmHjB/HM9kH9V8WlCpVUcdOmip9cjXOh6MxZ5yk1z2SjDUJfWmA== + dependencies: + "@types/retry" "^0.12.0" + retry "^0.12.0" + +p-timeout@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" + integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== + dependencies: + p-finally "^1.0.0" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + integrity sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M= + +psl@^1.1.28: + version "1.8.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@~2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" + integrity sha1-j5A0HmilPMySh4jaz80Rs265t44= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + +request@^2.86.0: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= + +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sax@0.5.x: + version "0.5.8" + resolved "https://registry.yarnpkg.com/sax/-/sax-0.5.8.tgz#d472db228eb331c2506b0e8c15524adb939d12c1" + integrity sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE= + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +stream-buffers@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-3.0.2.tgz#5249005a8d5c2d00b3a32e6e0a6ea209dc4f3521" + integrity sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ== + +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@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.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" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +typescript@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7" + integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg== + +underscore@~1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" + integrity sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI= + +universal-user-agent@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" + integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +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= + +uuid@^3.0.0, uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +validator@~9.4.1: + version "9.4.1" + resolved "https://registry.yarnpkg.com/validator/-/validator-9.4.1.tgz#abf466d398b561cd243050112c6ff1de6cc12663" + integrity sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +xml2js@0.2.8: + version "0.2.8" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.2.8.tgz#9b81690931631ff09d1957549faf54f4f980b3c2" + integrity sha1-m4FpCTFjH/CdGVdUn69U9PmAs8I= + dependencies: + sax "0.5.x" + +xmlbuilder@^9.0.7: + version "9.0.7" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= diff --git a/.github/classifier.json b/.github/classifier.json index 0ff0712c..96b58771 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -57,7 +57,7 @@ "editor-theming": {"assign": []}, "editor-wordnav": {"assign": ["alexdima"]}, "editor-wrapping": {"assign": ["alexdima"]}, - "emmet": {"assign": []}, + "emmet": {"assign": ["rzhao271"]}, "error-list": {"assign": ["sandy081"]}, "explorer-custom": {"assign": ["sandy081"]}, "extension-host": {"assign": []}, @@ -172,7 +172,7 @@ "workbench-tabs": {"assign": ["bpasero"]}, "workbench-touchbar": {"assign": ["bpasero"]}, "workbench-views": {"assign": ["sbatten"]}, - "workbench-welcome": {"assign": ["chrmarti"]}, + "workbench-welcome": {"assign": ["JacksonKearl"]}, "workbench-window": {"assign": ["bpasero"]}, "workbench-zen": {"assign": ["isidorn"]}, "workspace-edit": {"assign": ["jrieken"]}, diff --git a/.github/commands.json b/.github/commands.json index 980b2906..7563a392 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -81,7 +81,7 @@ "type": "label", "name": "*duplicate", "action": "close", - "comment": "Thanks for creating this issue! We figured it's covering the same as another one we already have. Thus, we closed this one as a duplicate. You can search for existing issues [here](https://aka.ms/vscodeissuesearch). See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!" + "comment": "Thanks for creating this issue! We figured it's covering the same as another one we already have. Thus, we closed this one as a duplicate. You can search for existing issues [here](${duplicateQuery}). See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!" }, { "type": "comment", diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a2523237..45f9ee8f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,6 +1,6 @@ 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 (sandbox) + { + target: '**/vs/**/electron-sandbox/**', + allowedTypes: CORE_TYPES, + disallowedDefinitions: [ + '@types/node' // no node.js + ] + }, + // Electron (renderer): skip + { + target: '**/vs/**/electron-browser/**', + skip: true // -> supports all types + }, + // Electron (main) + { + target: '**/vs/**/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, _b; + 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 + } + if ((_b = rule.disallowedTypes) === null || _b === void 0 ? void 0 : _b.some(disallowed => disallowed === text)) { + const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart()); + console.log(`[build/lib/layersChecker.ts]: Reference to '${text}' violates layer '${rule.target}' (${sourceFile.fileName} (${line + 1},${character + 1})`); + hasErrors = true; + return; + } + 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 index 1c757920..c0d67db6 100644 --- a/build/lib/layersChecker.ts +++ b/build/lib/layersChecker.ts @@ -25,8 +25,6 @@ import { match } from 'minimatch'; // 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', diff --git a/build/lib/monaco-api.js b/build/lib/monaco-api.js new file mode 100644 index 00000000..2c6677d4 --- /dev/null +++ b/build/lib/monaco-api.js @@ -0,0 +1,628 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.execute = exports.run3 = exports.DeclarationResolver = exports.FSProvider = exports.RECIPE_PATH = void 0; +const fs = require("fs"); +const path = require("path"); +const fancyLog = require("fancy-log"); +const ansiColors = require("ansi-colors"); +const dtsv = '3'; +const tsfmt = require('../../tsfmt.json'); +const SRC = path.join(__dirname, '../../src'); +exports.RECIPE_PATH = path.join(__dirname, '../monaco/monaco.d.ts.recipe'); +const DECLARATION_PATH = path.join(__dirname, '../../src/vs/monaco.d.ts'); +function logErr(message, ...rest) { + fancyLog(ansiColors.yellow(`[monaco.d.ts]`), message, ...rest); +} +function isDeclaration(ts, a) { + return (a.kind === ts.SyntaxKind.InterfaceDeclaration + || a.kind === ts.SyntaxKind.EnumDeclaration + || a.kind === ts.SyntaxKind.ClassDeclaration + || a.kind === ts.SyntaxKind.TypeAliasDeclaration + || a.kind === ts.SyntaxKind.FunctionDeclaration + || a.kind === ts.SyntaxKind.ModuleDeclaration); +} +function visitTopLevelDeclarations(ts, sourceFile, visitor) { + let stop = false; + let visit = (node) => { + if (stop) { + return; + } + switch (node.kind) { + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.VariableStatement: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.ModuleDeclaration: + stop = visitor(node); + } + if (stop) { + return; + } + ts.forEachChild(node, visit); + }; + visit(sourceFile); +} +function getAllTopLevelDeclarations(ts, sourceFile) { + let all = []; + visitTopLevelDeclarations(ts, sourceFile, (node) => { + if (node.kind === ts.SyntaxKind.InterfaceDeclaration || node.kind === ts.SyntaxKind.ClassDeclaration || node.kind === ts.SyntaxKind.ModuleDeclaration) { + let interfaceDeclaration = node; + let triviaStart = interfaceDeclaration.pos; + let triviaEnd = interfaceDeclaration.name.pos; + let triviaText = getNodeText(sourceFile, { pos: triviaStart, end: triviaEnd }); + if (triviaText.indexOf('@internal') === -1) { + all.push(node); + } + } + else { + let nodeText = getNodeText(sourceFile, node); + if (nodeText.indexOf('@internal') === -1) { + all.push(node); + } + } + return false /*continue*/; + }); + return all; +} +function getTopLevelDeclaration(ts, sourceFile, typeName) { + let result = null; + visitTopLevelDeclarations(ts, sourceFile, (node) => { + if (isDeclaration(ts, node) && node.name) { + if (node.name.text === typeName) { + result = node; + return true /*stop*/; + } + return false /*continue*/; + } + // node is ts.VariableStatement + if (getNodeText(sourceFile, node).indexOf(typeName) >= 0) { + result = node; + return true /*stop*/; + } + return false /*continue*/; + }); + return result; +} +function getNodeText(sourceFile, node) { + return sourceFile.getFullText().substring(node.pos, node.end); +} +function hasModifier(modifiers, kind) { + if (modifiers) { + for (let i = 0; i < modifiers.length; i++) { + let mod = modifiers[i]; + if (mod.kind === kind) { + return true; + } + } + } + return false; +} +function isStatic(ts, member) { + return hasModifier(member.modifiers, ts.SyntaxKind.StaticKeyword); +} +function isDefaultExport(ts, declaration) { + return (hasModifier(declaration.modifiers, ts.SyntaxKind.DefaultKeyword) + && hasModifier(declaration.modifiers, ts.SyntaxKind.ExportKeyword)); +} +function getMassagedTopLevelDeclarationText(ts, sourceFile, declaration, importName, usage, enums) { + let result = getNodeText(sourceFile, declaration); + if (declaration.kind === ts.SyntaxKind.InterfaceDeclaration || declaration.kind === ts.SyntaxKind.ClassDeclaration) { + let interfaceDeclaration = declaration; + const staticTypeName = (isDefaultExport(ts, interfaceDeclaration) + ? `${importName}.default` + : `${importName}.${declaration.name.text}`); + let instanceTypeName = staticTypeName; + const typeParametersCnt = (interfaceDeclaration.typeParameters ? interfaceDeclaration.typeParameters.length : 0); + if (typeParametersCnt > 0) { + let arr = []; + for (let i = 0; i < typeParametersCnt; i++) { + arr.push('any'); + } + instanceTypeName = `${instanceTypeName}<${arr.join(',')}>`; + } + const members = interfaceDeclaration.members; + members.forEach((member) => { + try { + let memberText = getNodeText(sourceFile, member); + if (memberText.indexOf('@internal') >= 0 || memberText.indexOf('private') >= 0) { + result = result.replace(memberText, ''); + } + else { + const memberName = member.name.text; + const memberAccess = (memberName.indexOf('.') >= 0 ? `['${memberName}']` : `.${memberName}`); + if (isStatic(ts, member)) { + usage.push(`a = ${staticTypeName}${memberAccess};`); + } + else { + usage.push(`a = (<${instanceTypeName}>b)${memberAccess};`); + } + } + } + catch (err) { + // 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 = []; + 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({ + enumName: declaration.name.getText(sourceFile), + text: result + }); + } + return result; +} +function format(ts, text, endl) { + const REALLY_FORMAT = false; + text = preformat(text, endl); + if (!REALLY_FORMAT) { + return text; + } + // Parse the source text + let sourceFile = ts.createSourceFile('file.ts', text, ts.ScriptTarget.Latest, /*setParentPointers*/ true); + // Get the formatting edits on the input sources + let edits = ts.formatting.formatDocument(sourceFile, getRuleProvider(tsfmt), tsfmt); + // Apply the edits on the input code + return applyEdits(text, edits); + function countParensCurly(text) { + let cnt = 0; + for (let i = 0; i < text.length; i++) { + if (text.charAt(i) === '(' || text.charAt(i) === '{') { + cnt++; + } + if (text.charAt(i) === ')' || text.charAt(i) === '}') { + cnt--; + } + } + return cnt; + } + function repeatStr(s, cnt) { + let r = ''; + for (let i = 0; i < cnt; i++) { + r += s; + } + return r; + } + function preformat(text, endl) { + let lines = text.split(endl); + let inComment = false; + let inCommentDeltaIndent = 0; + let indent = 0; + for (let i = 0; i < lines.length; i++) { + let line = lines[i].replace(/\s$/, ''); + let repeat = false; + let lineIndent = 0; + do { + repeat = false; + if (line.substring(0, 4) === ' ') { + line = line.substring(4); + lineIndent++; + repeat = true; + } + if (line.charAt(0) === '\t') { + line = line.substring(1); + lineIndent++; + repeat = true; + } + } while (repeat); + if (line.length === 0) { + continue; + } + if (inComment) { + if (/\*\//.test(line)) { + inComment = false; + } + lines[i] = repeatStr('\t', lineIndent + inCommentDeltaIndent) + line; + continue; + } + if (/\/\*/.test(line)) { + inComment = true; + inCommentDeltaIndent = indent - lineIndent; + lines[i] = repeatStr('\t', indent) + line; + continue; + } + const cnt = countParensCurly(line); + let shouldUnindentAfter = false; + let shouldUnindentBefore = false; + if (cnt < 0) { + if (/[({]/.test(line)) { + shouldUnindentAfter = true; + } + else { + shouldUnindentBefore = true; + } + } + else if (cnt === 0) { + shouldUnindentBefore = /^\}/.test(line); + } + let shouldIndentAfter = false; + if (cnt > 0) { + shouldIndentAfter = true; + } + else if (cnt === 0) { + shouldIndentAfter = /{$/.test(line); + } + if (shouldUnindentBefore) { + indent--; + } + lines[i] = repeatStr('\t', indent) + line; + if (shouldUnindentAfter) { + indent--; + } + if (shouldIndentAfter) { + indent++; + } + } + return lines.join(endl); + } + function getRuleProvider(options) { + // Share this between multiple formatters using the same options. + // This represents the bulk of the space the formatter uses. + return ts.formatting.getFormatContext(options); + } + function applyEdits(text, edits) { + // Apply edits in reverse on the existing text + let result = text; + for (let i = edits.length - 1; i >= 0; i--) { + let change = edits[i]; + let head = result.slice(0, change.span.start); + let tail = result.slice(change.span.start + change.span.length); + result = head + change.newText + tail; + } + 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(';'); + let directives = []; + rawDirectives.forEach((rawDirective) => { + if (rawDirective.length === 0) { + return; + } + let pieces = rawDirective.split('=>'); + let findStr = pieces[0]; + let replaceStr = pieces[1]; + findStr = findStr.replace(/[\-\\\{\}\*\+\?\|\^\$\.\,\[\]\(\)\#\s]/g, '\\$&'); + findStr = '\\b' + findStr + '\\b'; + directives.push([new RegExp(findStr, 'g'), replaceStr]); + }); + return createReplacerFromDirectives(directives); +} +function generateDeclarationFile(ts, recipe, sourceFileGetter) { + const endl = /\r\n/.test(recipe) ? '\r\n' : '\n'; + let lines = recipe.split(endl); + let result = []; + let usageCounter = 0; + let usageImports = []; + let usage = []; + let failed = false; + usage.push(`var a: any;`); + usage.push(`var b: any;`); + const generateUsageImport = (moduleId) => { + let importName = 'm' + (++usageCounter); + usageImports.push(`import * as ${importName} from './${moduleId.replace(/\.d\.ts$/, '')}';`); + return importName; + }; + let enums = []; + let version = null; + lines.forEach(line => { + if (failed) { + return; + } + let m0 = line.match(/^\/\/dtsv=(\d+)$/); + if (m0) { + version = m0[1]; + } + let m1 = line.match(/^\s*#include\(([^;)]*)(;[^)]*)?\)\:(.*)$/); + if (m1) { + let moduleId = m1[1]; + const sourceFile = sourceFileGetter(moduleId); + if (!sourceFile) { + logErr(`While handling ${line}`); + logErr(`Cannot find ${moduleId}`); + failed = true; + return; + } + const importName = generateUsageImport(moduleId); + let replacer = createReplacer(m1[2]); + let typeNames = m1[3].split(/,/); + typeNames.forEach((typeName) => { + typeName = typeName.trim(); + if (typeName.length === 0) { + return; + } + let declaration = getTopLevelDeclaration(ts, sourceFile, typeName); + if (!declaration) { + logErr(`While handling ${line}`); + logErr(`Cannot find ${typeName}`); + failed = true; + return; + } + result.push(replacer(getMassagedTopLevelDeclarationText(ts, sourceFile, declaration, importName, usage, enums))); + }); + return; + } + let m2 = line.match(/^\s*#includeAll\(([^;)]*)(;[^)]*)?\)\:(.*)$/); + if (m2) { + let moduleId = m2[1]; + const sourceFile = sourceFileGetter(moduleId); + if (!sourceFile) { + logErr(`While handling ${line}`); + logErr(`Cannot find ${moduleId}`); + failed = true; + return; + } + const importName = generateUsageImport(moduleId); + let replacer = createReplacer(m2[2]); + let typeNames = m2[3].split(/,/); + let typesToExcludeMap = {}; + let typesToExcludeArr = []; + typeNames.forEach((typeName) => { + typeName = typeName.trim(); + if (typeName.length === 0) { + return; + } + typesToExcludeMap[typeName] = true; + typesToExcludeArr.push(typeName); + }); + getAllTopLevelDeclarations(ts, sourceFile).forEach((declaration) => { + if (isDeclaration(ts, declaration) && declaration.name) { + if (typesToExcludeMap[declaration.name.text]) { + return; + } + } + else { + // node is ts.VariableStatement + let nodeText = getNodeText(sourceFile, declaration); + for (let i = 0; i < typesToExcludeArr.length; i++) { + if (nodeText.indexOf(typesToExcludeArr[i]) >= 0) { + return; + } + } + } + result.push(replacer(getMassagedTopLevelDeclarationText(ts, sourceFile, declaration, importName, usage, enums))); + }); + return; + } + result.push(line); + }); + if (failed) { + return null; + } + if (version !== dtsv) { + if (!version) { + logErr(`gulp watch restart required. 'monaco.d.ts.recipe' is written before versioning was introduced.`); + } + else { + logErr(`gulp watch restart required. 'monaco.d.ts.recipe' v${version} does not match runtime v${dtsv}.`); + } + return null; + } + let resultTxt = result.join(endl); + resultTxt = resultTxt.replace(/\bURI\b/g, 'Uri'); + resultTxt = resultTxt.replace(/\bEvent { + 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.', + ' * Licensed under the MIT License. See License.txt in the project root for license information.', + ' *--------------------------------------------------------------------------------------------*/', + '', + '// THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY.', + '' + ].concat(enums.map(e => e.text)).join(endl); + resultEnums = resultEnums.split(/\r\n|\n|\r/).join(endl); + resultEnums = format(ts, resultEnums, endl); + resultEnums = resultEnums.split(/\r\n|\n|\r/).join(endl); + return { + result: resultTxt, + usageContent: `${usageImports.join('\n')}\n\n${usage.join('\n')}`, + enums: resultEnums + }; +} +function _run(ts, sourceFileGetter) { + const recipe = fs.readFileSync(exports.RECIPE_PATH).toString(); + const t = generateDeclarationFile(ts, recipe, sourceFileGetter); + if (!t) { + return null; + } + const result = t.result; + const usageContent = t.usageContent; + const enums = t.enums; + const currentContent = fs.readFileSync(DECLARATION_PATH).toString(); + const one = currentContent.replace(/\r\n/gm, '\n'); + const other = result.replace(/\r\n/gm, '\n'); + const isTheSame = (one === other); + return { + content: result, + usageContent: usageContent, + enums: enums, + filePath: DECLARATION_PATH, + isTheSame + }; +} +class FSProvider { + existsSync(filePath) { + return fs.existsSync(filePath); + } + statSync(filePath) { + return fs.statSync(filePath); + } + readFileSync(_moduleId, filePath) { + return fs.readFileSync(filePath); + } +} +exports.FSProvider = FSProvider; +class CacheEntry { + constructor(sourceFile, mtime) { + this.sourceFile = sourceFile; + this.mtime = mtime; + } +} +class DeclarationResolver { + constructor(_fsProvider) { + this._fsProvider = _fsProvider; + this.ts = require('typescript'); + this._sourceFileCache = Object.create(null); + } + invalidateCache(moduleId) { + this._sourceFileCache[moduleId] = null; + } + getDeclarationSourceFile(moduleId) { + if (this._sourceFileCache[moduleId]) { + // Since we cannot trust file watching to invalidate the cache, check also the mtime + const fileName = this._getFileName(moduleId); + const mtime = this._fsProvider.statSync(fileName).mtime.getTime(); + if (this._sourceFileCache[moduleId].mtime !== mtime) { + this._sourceFileCache[moduleId] = null; + } + } + if (!this._sourceFileCache[moduleId]) { + this._sourceFileCache[moduleId] = this._getDeclarationSourceFile(moduleId); + } + return this._sourceFileCache[moduleId] ? this._sourceFileCache[moduleId].sourceFile : null; + } + _getFileName(moduleId) { + if (/\.d\.ts$/.test(moduleId)) { + return path.join(SRC, moduleId); + } + return path.join(SRC, `${moduleId}.ts`); + } + _getDeclarationSourceFile(moduleId) { + const fileName = this._getFileName(moduleId); + if (!this._fsProvider.existsSync(fileName)) { + return null; + } + const mtime = this._fsProvider.statSync(fileName).mtime.getTime(); + if (/\.d\.ts$/.test(moduleId)) { + // const mtime = this._fsProvider.statFileSync() + const fileContents = this._fsProvider.readFileSync(moduleId, fileName).toString(); + return new CacheEntry(this.ts.createSourceFile(fileName, fileContents, this.ts.ScriptTarget.ES5), mtime); + } + const fileContents = this._fsProvider.readFileSync(moduleId, fileName).toString(); + const fileMap = { + 'file.ts': fileContents + }; + const service = this.ts.createLanguageService(new TypeScriptLanguageServiceHost(this.ts, {}, fileMap, {})); + const text = service.getEmitOutput('file.ts', true, true).outputFiles[0].text; + return new CacheEntry(this.ts.createSourceFile(fileName, text, this.ts.ScriptTarget.ES5), mtime); + } +} +exports.DeclarationResolver = DeclarationResolver; +function run3(resolver) { + const sourceFileGetter = (moduleId) => resolver.getDeclarationSourceFile(moduleId); + return _run(resolver.ts, sourceFileGetter); +} +exports.run3 = run3; +class TypeScriptLanguageServiceHost { + constructor(ts, libs, files, compilerOptions) { + this._ts = ts; + this._libs = libs; + this._files = files; + this._compilerOptions = compilerOptions; + } + // --- language service host --------------- + getCompilationSettings() { + return this._compilerOptions; + } + getScriptFileNames() { + return ([] + .concat(Object.keys(this._libs)) + .concat(Object.keys(this._files))); + } + getScriptVersion(_fileName) { + return '1'; + } + getProjectVersion() { + return '1'; + } + getScriptSnapshot(fileName) { + if (this._files.hasOwnProperty(fileName)) { + return this._ts.ScriptSnapshot.fromString(this._files[fileName]); + } + else if (this._libs.hasOwnProperty(fileName)) { + return this._ts.ScriptSnapshot.fromString(this._libs[fileName]); + } + else { + return this._ts.ScriptSnapshot.fromString(''); + } + } + getScriptKind(_fileName) { + return this._ts.ScriptKind.TS; + } + getCurrentDirectory() { + return ''; + } + getDefaultLibFileName(_options) { + return 'defaultLib:es5'; + } + isDefaultLibFileName(fileName) { + return fileName === this.getDefaultLibFileName(this._compilerOptions); + } +} +function execute() { + let r = run3(new DeclarationResolver(new FSProvider())); + if (!r) { + throw new Error(`monaco.d.ts generation error - Cannot continue`); + } + return r; +} +exports.execute = execute; diff --git a/build/lib/monaco-api.ts b/build/lib/monaco-api.ts index 6604b14d..e45422b5 100644 --- a/build/lib/monaco-api.ts +++ b/build/lib/monaco-api.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; -import * as ts from 'typescript'; +import type * as ts from 'typescript'; import * as path from 'path'; import * as fancyLog from 'fancy-log'; import * as ansiColors from 'ansi-colors'; @@ -26,7 +26,7 @@ type SourceFileGetter = (moduleId: string) => ts.SourceFile | null; type TSTopLevelDeclaration = ts.InterfaceDeclaration | ts.EnumDeclaration | ts.ClassDeclaration | ts.TypeAliasDeclaration | ts.FunctionDeclaration | ts.ModuleDeclaration; type TSTopLevelDeclare = TSTopLevelDeclaration | ts.VariableStatement; -function isDeclaration(a: TSTopLevelDeclare): a is TSTopLevelDeclaration { +function isDeclaration(ts: typeof import('typescript'), a: TSTopLevelDeclare): a is TSTopLevelDeclaration { return ( a.kind === ts.SyntaxKind.InterfaceDeclaration || a.kind === ts.SyntaxKind.EnumDeclaration @@ -37,7 +37,7 @@ function isDeclaration(a: TSTopLevelDeclare): a is TSTopLevelDeclaration { ); } -function visitTopLevelDeclarations(sourceFile: ts.SourceFile, visitor: (node: TSTopLevelDeclare) => boolean): void { +function visitTopLevelDeclarations(ts: typeof import('typescript'), sourceFile: ts.SourceFile, visitor: (node: TSTopLevelDeclare) => boolean): void { let stop = false; let visit = (node: ts.Node): void => { @@ -66,9 +66,9 @@ function visitTopLevelDeclarations(sourceFile: ts.SourceFile, visitor: (node: TS } -function getAllTopLevelDeclarations(sourceFile: ts.SourceFile): TSTopLevelDeclare[] { +function getAllTopLevelDeclarations(ts: typeof import('typescript'), sourceFile: ts.SourceFile): TSTopLevelDeclare[] { let all: TSTopLevelDeclare[] = []; - visitTopLevelDeclarations(sourceFile, (node) => { + visitTopLevelDeclarations(ts, sourceFile, (node) => { if (node.kind === ts.SyntaxKind.InterfaceDeclaration || node.kind === ts.SyntaxKind.ClassDeclaration || node.kind === ts.SyntaxKind.ModuleDeclaration) { let interfaceDeclaration = node; let triviaStart = interfaceDeclaration.pos; @@ -90,10 +90,10 @@ function getAllTopLevelDeclarations(sourceFile: ts.SourceFile): TSTopLevelDeclar } -function getTopLevelDeclaration(sourceFile: ts.SourceFile, typeName: string): TSTopLevelDeclare | null { +function getTopLevelDeclaration(ts: typeof import('typescript'), sourceFile: ts.SourceFile, typeName: string): TSTopLevelDeclare | null { let result: TSTopLevelDeclare | null = null; - visitTopLevelDeclarations(sourceFile, (node) => { - if (isDeclaration(node) && node.name) { + visitTopLevelDeclarations(ts, sourceFile, (node) => { + if (isDeclaration(ts, node) && node.name) { if (node.name.text === typeName) { result = node; return true /*stop*/; @@ -127,24 +127,24 @@ function hasModifier(modifiers: ts.NodeArray | undefined, kind: ts. return false; } -function isStatic(member: ts.ClassElement | ts.TypeElement): boolean { +function isStatic(ts: typeof import('typescript'), member: ts.ClassElement | ts.TypeElement): boolean { return hasModifier(member.modifiers, ts.SyntaxKind.StaticKeyword); } -function isDefaultExport(declaration: ts.InterfaceDeclaration | ts.ClassDeclaration): boolean { +function isDefaultExport(ts: typeof import('typescript'), declaration: ts.InterfaceDeclaration | ts.ClassDeclaration): boolean { return ( hasModifier(declaration.modifiers, ts.SyntaxKind.DefaultKeyword) && hasModifier(declaration.modifiers, ts.SyntaxKind.ExportKeyword) ); } -function getMassagedTopLevelDeclarationText(sourceFile: ts.SourceFile, declaration: TSTopLevelDeclare, importName: string, usage: string[], enums: IEnumEntry[]): string { +function getMassagedTopLevelDeclarationText(ts: typeof import('typescript'), 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; const staticTypeName = ( - isDefaultExport(interfaceDeclaration) + isDefaultExport(ts, interfaceDeclaration) ? `${importName}.default` : `${importName}.${declaration.name!.text}` ); @@ -168,7 +168,7 @@ function getMassagedTopLevelDeclarationText(sourceFile: ts.SourceFile, declarati } else { const memberName = (member.name).text; const memberAccess = (memberName.indexOf('.') >= 0 ? `['${memberName}']` : `.${memberName}`); - if (isStatic(member)) { + if (isStatic(ts, member)) { usage.push(`a = ${staticTypeName}${memberAccess};`); } else { usage.push(`a = (<${instanceTypeName}>b)${memberAccess};`); @@ -222,7 +222,7 @@ function getMassagedTopLevelDeclarationText(sourceFile: ts.SourceFile, declarati return result; } -function format(text: string, endl: string): string { +function format(ts: typeof import('typescript'), text: string, endl: string): string { const REALLY_FORMAT = false; text = preformat(text, endl); @@ -396,7 +396,7 @@ interface IEnumEntry { text: string; } -function generateDeclarationFile(recipe: string, sourceFileGetter: SourceFileGetter): ITempResult | null { +function generateDeclarationFile(ts: typeof import('typescript'), recipe: string, sourceFileGetter: SourceFileGetter): ITempResult | null { const endl = /\r\n/.test(recipe) ? '\r\n' : '\n'; let lines = recipe.split(endl); @@ -452,14 +452,14 @@ function generateDeclarationFile(recipe: string, sourceFileGetter: SourceFileGet if (typeName.length === 0) { return; } - let declaration = getTopLevelDeclaration(sourceFile, typeName); + let declaration = getTopLevelDeclaration(ts, sourceFile, typeName); if (!declaration) { logErr(`While handling ${line}`); logErr(`Cannot find ${typeName}`); failed = true; return; } - result.push(replacer(getMassagedTopLevelDeclarationText(sourceFile, declaration, importName, usage, enums))); + result.push(replacer(getMassagedTopLevelDeclarationText(ts, sourceFile, declaration, importName, usage, enums))); }); return; } @@ -491,8 +491,8 @@ function generateDeclarationFile(recipe: string, sourceFileGetter: SourceFileGet typesToExcludeArr.push(typeName); }); - getAllTopLevelDeclarations(sourceFile).forEach((declaration) => { - if (isDeclaration(declaration) && declaration.name) { + getAllTopLevelDeclarations(ts, sourceFile).forEach((declaration) => { + if (isDeclaration(ts, declaration) && declaration.name) { if (typesToExcludeMap[declaration.name.text]) { return; } @@ -505,7 +505,7 @@ function generateDeclarationFile(recipe: string, sourceFileGetter: SourceFileGet } } } - result.push(replacer(getMassagedTopLevelDeclarationText(sourceFile, declaration, importName, usage, enums))); + result.push(replacer(getMassagedTopLevelDeclarationText(ts, sourceFile, declaration, importName, usage, enums))); }); return; } @@ -530,7 +530,7 @@ function generateDeclarationFile(recipe: string, sourceFileGetter: SourceFileGet resultTxt = resultTxt.replace(/\bURI\b/g, 'Uri'); resultTxt = resultTxt.replace(/\bEvent { @@ -553,7 +553,7 @@ function generateDeclarationFile(recipe: string, sourceFileGetter: SourceFileGet '' ].concat(enums.map(e => e.text)).join(endl); resultEnums = resultEnums.split(/\r\n|\n|\r/).join(endl); - resultEnums = format(resultEnums, endl); + resultEnums = format(ts, resultEnums, endl); resultEnums = resultEnums.split(/\r\n|\n|\r/).join(endl); return { @@ -571,9 +571,9 @@ export interface IMonacoDeclarationResult { isTheSame: boolean; } -function _run(sourceFileGetter: SourceFileGetter): IMonacoDeclarationResult | null { +function _run(ts: typeof import('typescript'), sourceFileGetter: SourceFileGetter): IMonacoDeclarationResult | null { const recipe = fs.readFileSync(RECIPE_PATH).toString(); - const t = generateDeclarationFile(recipe, sourceFileGetter); + const t = generateDeclarationFile(ts, recipe, sourceFileGetter); if (!t) { return null; } @@ -617,9 +617,11 @@ class CacheEntry { export class DeclarationResolver { + public readonly ts: typeof import('typescript'); private _sourceFileCache: { [moduleId: string]: CacheEntry | null; }; constructor(private readonly _fsProvider: FSProvider) { + this.ts = require('typescript') as typeof import('typescript'); this._sourceFileCache = Object.create(null); } @@ -659,7 +661,7 @@ export class DeclarationResolver { // const mtime = this._fsProvider.statFileSync() const fileContents = this._fsProvider.readFileSync(moduleId, fileName).toString(); return new CacheEntry( - ts.createSourceFile(fileName, fileContents, ts.ScriptTarget.ES5), + this.ts.createSourceFile(fileName, fileContents, this.ts.ScriptTarget.ES5), mtime ); } @@ -667,10 +669,10 @@ export class DeclarationResolver { const fileMap: IFileMap = { 'file.ts': fileContents }; - const service = ts.createLanguageService(new TypeScriptLanguageServiceHost({}, fileMap, {})); + const service = this.ts.createLanguageService(new TypeScriptLanguageServiceHost(this.ts, {}, fileMap, {})); const text = service.getEmitOutput('file.ts', true, true).outputFiles[0].text; return new CacheEntry( - ts.createSourceFile(fileName, text, ts.ScriptTarget.ES5), + this.ts.createSourceFile(fileName, text, this.ts.ScriptTarget.ES5), mtime ); } @@ -678,7 +680,7 @@ export class DeclarationResolver { export function run3(resolver: DeclarationResolver): IMonacoDeclarationResult | null { const sourceFileGetter = (moduleId: string) => resolver.getDeclarationSourceFile(moduleId); - return _run(sourceFileGetter); + return _run(resolver.ts, sourceFileGetter); } @@ -689,11 +691,13 @@ interface IFileMap { [fileName: string]: string; } class TypeScriptLanguageServiceHost implements ts.LanguageServiceHost { + private readonly _ts: typeof import('typescript'); private readonly _libs: ILibMap; private readonly _files: IFileMap; private readonly _compilerOptions: ts.CompilerOptions; - constructor(libs: ILibMap, files: IFileMap, compilerOptions: ts.CompilerOptions) { + constructor(ts: typeof import('typescript'), libs: ILibMap, files: IFileMap, compilerOptions: ts.CompilerOptions) { + this._ts = ts; this._libs = libs; this._files = files; this._compilerOptions = compilerOptions; @@ -719,15 +723,15 @@ class TypeScriptLanguageServiceHost implements ts.LanguageServiceHost { } getScriptSnapshot(fileName: string): ts.IScriptSnapshot { if (this._files.hasOwnProperty(fileName)) { - return ts.ScriptSnapshot.fromString(this._files[fileName]); + return this._ts.ScriptSnapshot.fromString(this._files[fileName]); } else if (this._libs.hasOwnProperty(fileName)) { - return ts.ScriptSnapshot.fromString(this._libs[fileName]); + return this._ts.ScriptSnapshot.fromString(this._libs[fileName]); } else { - return ts.ScriptSnapshot.fromString(''); + return this._ts.ScriptSnapshot.fromString(''); } } getScriptKind(_fileName: string): ts.ScriptKind { - return ts.ScriptKind.TS; + return this._ts.ScriptKind.TS; } getCurrentDirectory(): string { return ''; diff --git a/build/lib/nls.js b/build/lib/nls.js new file mode 100644 index 00000000..671a0afe --- /dev/null +++ b/build/lib/nls.js @@ -0,0 +1,348 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.nls = void 0; +const lazy = require("lazy.js"); +const event_stream_1 = require("event-stream"); +const File = require("vinyl"); +const sm = require("source-map"); +const path = require("path"); +var CollectStepResult; +(function (CollectStepResult) { + CollectStepResult[CollectStepResult["Yes"] = 0] = "Yes"; + CollectStepResult[CollectStepResult["YesAndRecurse"] = 1] = "YesAndRecurse"; + CollectStepResult[CollectStepResult["No"] = 2] = "No"; + CollectStepResult[CollectStepResult["NoAndRecurse"] = 3] = "NoAndRecurse"; +})(CollectStepResult || (CollectStepResult = {})); +function collect(ts, node, fn) { + const result = []; + function loop(node) { + const stepResult = fn(node); + if (stepResult === CollectStepResult.Yes || stepResult === CollectStepResult.YesAndRecurse) { + result.push(node); + } + if (stepResult === CollectStepResult.YesAndRecurse || stepResult === CollectStepResult.NoAndRecurse) { + ts.forEachChild(node, loop); + } + } + loop(node); + return result; +} +function clone(object) { + const result = {}; + for (const id in object) { + result[id] = object[id]; + } + return result; +} +function template(lines) { + let indent = '', wrap = ''; + if (lines.length > 1) { + indent = '\t'; + wrap = '\n'; + } + return `/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ +define([], [${wrap + lines.map(l => indent + l).join(',\n') + wrap}]);`; +} +/** + * Returns a stream containing the patched JavaScript and source maps. + */ +function nls() { + const input = event_stream_1.through(); + const output = input.pipe(event_stream_1.through(function (f) { + if (!f.sourceMap) { + return this.emit('error', new Error(`File ${f.relative} does not have sourcemaps.`)); + } + let source = f.sourceMap.sources[0]; + if (!source) { + return this.emit('error', new Error(`File ${f.relative} does not have a source in the source map.`)); + } + const root = f.sourceMap.sourceRoot; + if (root) { + source = path.join(root, source); + } + const typescript = f.sourceMap.sourcesContent[0]; + if (!typescript) { + return this.emit('error', new Error(`File ${f.relative} does not have the original content in the source map.`)); + } + _nls.patchFiles(f, typescript).forEach(f => this.emit('data', f)); + })); + return event_stream_1.duplex(input, output); +} +exports.nls = nls; +function isImportNode(ts, node) { + return node.kind === ts.SyntaxKind.ImportDeclaration || node.kind === ts.SyntaxKind.ImportEqualsDeclaration; +} +var _nls; +(function (_nls) { + function fileFrom(file, contents, path = file.path) { + return new File({ + contents: Buffer.from(contents), + base: file.base, + cwd: file.cwd, + path: path + }); + } + function mappedPositionFrom(source, lc) { + return { source, line: lc.line + 1, column: lc.character }; + } + function lcFrom(position) { + return { line: position.line - 1, character: position.column }; + } + class SingleFileServiceHost { + constructor(ts, options, filename, contents) { + this.options = options; + this.filename = filename; + this.getCompilationSettings = () => this.options; + this.getScriptFileNames = () => [this.filename]; + this.getScriptVersion = () => '1'; + this.getScriptSnapshot = (name) => name === this.filename ? this.file : this.lib; + this.getCurrentDirectory = () => ''; + this.getDefaultLibFileName = () => 'lib.d.ts'; + this.file = ts.ScriptSnapshot.fromString(contents); + this.lib = ts.ScriptSnapshot.fromString(''); + } + } + function isCallExpressionWithinTextSpanCollectStep(ts, textSpan, node) { + if (!ts.textSpanContainsTextSpan({ start: node.pos, length: node.end - node.pos }, textSpan)) { + return CollectStepResult.No; + } + return node.kind === ts.SyntaxKind.CallExpression ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse; + } + function analyze(ts, contents, options = {}) { + const filename = 'file.ts'; + const serviceHost = new SingleFileServiceHost(ts, Object.assign(clone(options), { noResolve: true }), filename, contents); + const service = ts.createLanguageService(serviceHost); + const sourceFile = ts.createSourceFile(filename, contents, ts.ScriptTarget.ES5, true); + // all imports + const imports = lazy(collect(ts, sourceFile, n => isImportNode(ts, n) ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse)); + // import nls = require('vs/nls'); + const importEqualsDeclarations = imports + .filter(n => n.kind === ts.SyntaxKind.ImportEqualsDeclaration) + .map(n => n) + .filter(d => d.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) + .filter(d => d.moduleReference.expression.getText() === '\'vs/nls\''); + // import ... from 'vs/nls'; + const importDeclarations = imports + .filter(n => n.kind === ts.SyntaxKind.ImportDeclaration) + .map(n => n) + .filter(d => d.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral) + .filter(d => d.moduleSpecifier.getText() === '\'vs/nls\'') + .filter(d => !!d.importClause && !!d.importClause.namedBindings); + const nlsExpressions = importEqualsDeclarations + .map(d => d.moduleReference.expression) + .concat(importDeclarations.map(d => d.moduleSpecifier)) + .map(d => ({ + start: ts.getLineAndCharacterOfPosition(sourceFile, d.getStart()), + end: ts.getLineAndCharacterOfPosition(sourceFile, d.getEnd()) + })); + // `nls.localize(...)` calls + const nlsLocalizeCallExpressions = importDeclarations + .filter(d => !!(d.importClause && d.importClause.namedBindings && d.importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport)) + .map(d => d.importClause.namedBindings.name) + .concat(importEqualsDeclarations.map(d => d.name)) + // find read-only references to `nls` + .map(n => service.getReferencesAtPosition(filename, n.pos + 1)) + .flatten() + .filter(r => !r.isWriteAccess) + // find the deepest call expressions AST nodes that contain those references + .map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n))) + .map(a => lazy(a).last()) + .filter(n => !!n) + .map(n => n) + // only `localize` calls + .filter(n => n.expression.kind === ts.SyntaxKind.PropertyAccessExpression && n.expression.name.getText() === 'localize'); + // `localize` named imports + const allLocalizeImportDeclarations = importDeclarations + .filter(d => !!(d.importClause && d.importClause.namedBindings && d.importClause.namedBindings.kind === ts.SyntaxKind.NamedImports)) + .map(d => [].concat(d.importClause.namedBindings.elements)) + .flatten(); + // `localize` read-only references + const localizeReferences = allLocalizeImportDeclarations + .filter(d => d.name.getText() === 'localize') + .map(n => service.getReferencesAtPosition(filename, n.pos + 1)) + .flatten() + .filter(r => !r.isWriteAccess); + // custom named `localize` read-only references + const namedLocalizeReferences = allLocalizeImportDeclarations + .filter(d => d.propertyName && d.propertyName.getText() === 'localize') + .map(n => service.getReferencesAtPosition(filename, n.name.pos + 1)) + .flatten() + .filter(r => !r.isWriteAccess); + // find the deepest call expressions AST nodes that contain those references + const localizeCallExpressions = localizeReferences + .concat(namedLocalizeReferences) + .map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n))) + .map(a => lazy(a).last()) + .filter(n => !!n) + .map(n => n); + // collect everything + const localizeCalls = nlsLocalizeCallExpressions + .concat(localizeCallExpressions) + .map(e => e.arguments) + .filter(a => a.length > 1) + .sort((a, b) => a[0].getStart() - b[0].getStart()) + .map(a => ({ + keySpan: { start: ts.getLineAndCharacterOfPosition(sourceFile, a[0].getStart()), end: ts.getLineAndCharacterOfPosition(sourceFile, a[0].getEnd()) }, + key: a[0].getText(), + valueSpan: { start: ts.getLineAndCharacterOfPosition(sourceFile, a[1].getStart()), end: ts.getLineAndCharacterOfPosition(sourceFile, a[1].getEnd()) }, + value: a[1].getText() + })); + return { + localizeCalls: localizeCalls.toArray(), + nlsExpressions: nlsExpressions.toArray() + }; + } + class TextModel { + constructor(contents) { + const regex = /\r\n|\r|\n/g; + let index = 0; + let match; + this.lines = []; + this.lineEndings = []; + while (match = regex.exec(contents)) { + this.lines.push(contents.substring(index, match.index)); + this.lineEndings.push(match[0]); + index = regex.lastIndex; + } + if (contents.length > 0) { + this.lines.push(contents.substring(index, contents.length)); + this.lineEndings.push(''); + } + } + get(index) { + return this.lines[index]; + } + set(index, line) { + this.lines[index] = line; + } + get lineCount() { + return this.lines.length; + } + /** + * Applies patch(es) to the model. + * Multiple patches must be ordered. + * Does not support patches spanning multiple lines. + */ + apply(patch) { + const startLineNumber = patch.span.start.line; + const endLineNumber = patch.span.end.line; + const startLine = this.lines[startLineNumber] || ''; + const endLine = this.lines[endLineNumber] || ''; + this.lines[startLineNumber] = [ + startLine.substring(0, patch.span.start.character), + patch.content, + endLine.substring(patch.span.end.character) + ].join(''); + for (let i = startLineNumber + 1; i <= endLineNumber; i++) { + this.lines[i] = ''; + } + } + toString() { + return lazy(this.lines).zip(this.lineEndings) + .flatten().toArray().join(''); + } + } + function patchJavascript(patches, contents, moduleId) { + const model = new TextModel(contents); + // patch the localize calls + lazy(patches).reverse().each(p => model.apply(p)); + // patch the 'vs/nls' imports + const firstLine = model.get(0); + const patchedFirstLine = firstLine.replace(/(['"])vs\/nls\1/g, `$1vs/nls!${moduleId}$1`); + model.set(0, patchedFirstLine); + return model.toString(); + } + function patchSourcemap(patches, rsm, smc) { + const smg = new sm.SourceMapGenerator({ + file: rsm.file, + sourceRoot: rsm.sourceRoot + }); + patches = patches.reverse(); + let currentLine = -1; + let currentLineDiff = 0; + let source = null; + smc.eachMapping(m => { + const patch = patches[patches.length - 1]; + const original = { line: m.originalLine, column: m.originalColumn }; + const generated = { line: m.generatedLine, column: m.generatedColumn }; + if (currentLine !== generated.line) { + currentLineDiff = 0; + } + currentLine = generated.line; + generated.column += currentLineDiff; + if (patch && m.generatedLine - 1 === patch.span.end.line && m.generatedColumn === patch.span.end.character) { + const originalLength = patch.span.end.character - patch.span.start.character; + const modifiedLength = patch.content.length; + const lengthDiff = modifiedLength - originalLength; + currentLineDiff += lengthDiff; + generated.column += lengthDiff; + patches.pop(); + } + source = rsm.sourceRoot ? path.relative(rsm.sourceRoot, m.source) : m.source; + source = source.replace(/\\/g, '/'); + smg.addMapping({ source, name: m.name, original, generated }); + }, null, sm.SourceMapConsumer.GENERATED_ORDER); + if (source) { + smg.setSourceContent(source, smc.sourceContentFor(source)); + } + return JSON.parse(smg.toString()); + } + function patch(ts, moduleId, typescript, javascript, sourcemap) { + const { localizeCalls, nlsExpressions } = analyze(ts, typescript); + if (localizeCalls.length === 0) { + return { javascript, sourcemap }; + } + const nlsKeys = template(localizeCalls.map(lc => lc.key)); + const nls = template(localizeCalls.map(lc => lc.value)); + const smc = new sm.SourceMapConsumer(sourcemap); + const positionFrom = mappedPositionFrom.bind(null, sourcemap.sources[0]); + let i = 0; + // build patches + const patches = lazy(localizeCalls) + .map(lc => ([ + { range: lc.keySpan, content: '' + (i++) }, + { range: lc.valueSpan, content: 'null' } + ])) + .flatten() + .map(c => { + const start = lcFrom(smc.generatedPositionFor(positionFrom(c.range.start))); + const end = lcFrom(smc.generatedPositionFor(positionFrom(c.range.end))); + return { span: { start, end }, content: c.content }; + }) + .toArray(); + javascript = patchJavascript(patches, javascript, moduleId); + // since imports are not within the sourcemap information, + // we must do this MacGyver style + if (nlsExpressions.length) { + javascript = javascript.replace(/^define\(.*$/m, line => { + return line.replace(/(['"])vs\/nls\1/g, `$1vs/nls!${moduleId}$1`); + }); + } + sourcemap = patchSourcemap(patches, sourcemap, smc); + return { javascript, sourcemap, nlsKeys, nls }; + } + function patchFiles(javascriptFile, typescript) { + const ts = require('typescript'); + // hack? + const moduleId = javascriptFile.relative + .replace(/\.js$/, '') + .replace(/\\/g, '/'); + const { javascript, sourcemap, nlsKeys, nls } = patch(ts, moduleId, typescript, javascriptFile.contents.toString(), javascriptFile.sourceMap); + const result = [fileFrom(javascriptFile, javascript)]; + result[0].sourceMap = sourcemap; + if (nlsKeys) { + result.push(fileFrom(javascriptFile, nlsKeys, javascriptFile.path.replace(/\.js$/, '.nls.keys.js'))); + } + if (nls) { + result.push(fileFrom(javascriptFile, nls, javascriptFile.path.replace(/\.js$/, '.nls.js'))); + } + return result; + } + _nls.patchFiles = patchFiles; +})(_nls || (_nls = {})); diff --git a/build/lib/nls.ts b/build/lib/nls.ts index bc667459..cd184ad9 100644 --- a/build/lib/nls.ts +++ b/build/lib/nls.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as ts from 'typescript'; +import type * as ts from 'typescript'; import * as lazy from 'lazy.js'; import { duplex, through } from 'event-stream'; import * as File from 'vinyl'; @@ -21,7 +21,7 @@ enum CollectStepResult { NoAndRecurse } -function collect(node: ts.Node, fn: (node: ts.Node) => CollectStepResult): ts.Node[] { +function collect(ts: typeof import('typescript'), node: ts.Node, fn: (node: ts.Node) => CollectStepResult): ts.Node[] { const result: ts.Node[] = []; function loop(node: ts.Node) { @@ -65,7 +65,7 @@ define([], [${ wrap + lines.map(l => indent + l).join(',\n') + wrap}]);`; /** * Returns a stream containing the patched JavaScript and source maps. */ -function nls(): NodeJS.ReadWriteStream { +export function nls(): NodeJS.ReadWriteStream { const input = through(); const output = input.pipe(through(function (f: FileSourceMap) { if (!f.sourceMap) { @@ -87,48 +87,48 @@ function nls(): NodeJS.ReadWriteStream { return this.emit('error', new Error(`File ${f.relative} does not have the original content in the source map.`)); } - nls.patchFiles(f, typescript).forEach(f => this.emit('data', f)); + _nls.patchFiles(f, typescript).forEach(f => this.emit('data', f)); })); return duplex(input, output); } -function isImportNode(node: ts.Node): boolean { +function isImportNode(ts: typeof import('typescript'), node: ts.Node): boolean { return node.kind === ts.SyntaxKind.ImportDeclaration || node.kind === ts.SyntaxKind.ImportEqualsDeclaration; } -module nls { +module _nls { - export interface INlsStringResult { + interface INlsStringResult { javascript: string; sourcemap: sm.RawSourceMap; nls?: string; nlsKeys?: string; } - export interface ISpan { + interface ISpan { start: ts.LineAndCharacter; end: ts.LineAndCharacter; } - export interface ILocalizeCall { + interface ILocalizeCall { keySpan: ISpan; key: string; valueSpan: ISpan; value: string; } - export interface ILocalizeAnalysisResult { + interface ILocalizeAnalysisResult { localizeCalls: ILocalizeCall[]; nlsExpressions: ISpan[]; } - export interface IPatch { + interface IPatch { span: ISpan; content: string; } - export function fileFrom(file: File, contents: string, path: string = file.path) { + function fileFrom(file: File, contents: string, path: string = file.path) { return new File({ contents: Buffer.from(contents), base: file.base, @@ -137,20 +137,20 @@ module nls { }); } - export function mappedPositionFrom(source: string, lc: ts.LineAndCharacter): sm.MappedPosition { + function mappedPositionFrom(source: string, lc: ts.LineAndCharacter): sm.MappedPosition { return { source, line: lc.line + 1, column: lc.character }; } - export function lcFrom(position: sm.Position): ts.LineAndCharacter { + function lcFrom(position: sm.Position): ts.LineAndCharacter { return { line: position.line - 1, character: position.column }; } - export class SingleFileServiceHost implements ts.LanguageServiceHost { + class SingleFileServiceHost implements ts.LanguageServiceHost { private file: ts.IScriptSnapshot; private lib: ts.IScriptSnapshot; - constructor(private options: ts.CompilerOptions, private filename: string, contents: string) { + constructor(ts: typeof import('typescript'), private options: ts.CompilerOptions, private filename: string, contents: string) { this.file = ts.ScriptSnapshot.fromString(contents); this.lib = ts.ScriptSnapshot.fromString(''); } @@ -163,7 +163,7 @@ module nls { getDefaultLibFileName = () => 'lib.d.ts'; } - function isCallExpressionWithinTextSpanCollectStep(textSpan: ts.TextSpan, node: ts.Node): CollectStepResult { + function isCallExpressionWithinTextSpanCollectStep(ts: typeof import('typescript'), textSpan: ts.TextSpan, node: ts.Node): CollectStepResult { if (!ts.textSpanContainsTextSpan({ start: node.pos, length: node.end - node.pos }, textSpan)) { return CollectStepResult.No; } @@ -171,14 +171,14 @@ module nls { return node.kind === ts.SyntaxKind.CallExpression ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse; } - export function analyze(contents: string, options: ts.CompilerOptions = {}): ILocalizeAnalysisResult { + function analyze(ts: typeof import('typescript'), contents: string, options: ts.CompilerOptions = {}): ILocalizeAnalysisResult { const filename = 'file.ts'; - const serviceHost = new SingleFileServiceHost(Object.assign(clone(options), { noResolve: true }), filename, contents); + const serviceHost = new SingleFileServiceHost(ts, Object.assign(clone(options), { noResolve: true }), filename, contents); const service = ts.createLanguageService(serviceHost); const sourceFile = ts.createSourceFile(filename, contents, ts.ScriptTarget.ES5, true); // all imports - const imports = lazy(collect(sourceFile, n => isImportNode(n) ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse)); + const imports = lazy(collect(ts, sourceFile, n => isImportNode(ts, n) ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse)); // import nls = require('vs/nls'); const importEqualsDeclarations = imports @@ -215,7 +215,7 @@ module nls { .filter(r => !r.isWriteAccess) // find the deepest call expressions AST nodes that contain those references - .map(r => collect(sourceFile, n => isCallExpressionWithinTextSpanCollectStep(r.textSpan, n))) + .map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n))) .map(a => lazy(a).last()) .filter(n => !!n) .map(n => n) @@ -246,7 +246,7 @@ module nls { // find the deepest call expressions AST nodes that contain those references const localizeCallExpressions = localizeReferences .concat(namedLocalizeReferences) - .map(r => collect(sourceFile, n => isCallExpressionWithinTextSpanCollectStep(r.textSpan, n))) + .map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n))) .map(a => lazy(a).last()) .filter(n => !!n) .map(n => n); @@ -270,7 +270,7 @@ module nls { }; } - export class TextModel { + class TextModel { private lines: string[]; private lineEndings: string[]; @@ -336,8 +336,8 @@ module nls { } } - export function patchJavascript(patches: IPatch[], contents: string, moduleId: string): string { - const model = new nls.TextModel(contents); + function patchJavascript(patches: IPatch[], contents: string, moduleId: string): string { + const model = new TextModel(contents); // patch the localize calls lazy(patches).reverse().each(p => model.apply(p)); @@ -350,7 +350,7 @@ module nls { return model.toString(); } - export function patchSourcemap(patches: IPatch[], rsm: sm.RawSourceMap, smc: sm.SourceMapConsumer): sm.RawSourceMap { + function patchSourcemap(patches: IPatch[], rsm: sm.RawSourceMap, smc: sm.SourceMapConsumer): sm.RawSourceMap { const smg = new sm.SourceMapGenerator({ file: rsm.file, sourceRoot: rsm.sourceRoot @@ -395,8 +395,8 @@ module nls { return JSON.parse(smg.toString()); } - export function patch(moduleId: string, typescript: string, javascript: string, sourcemap: sm.RawSourceMap): INlsStringResult { - const { localizeCalls, nlsExpressions } = analyze(typescript); + function patch(ts: typeof import('typescript'), moduleId: string, typescript: string, javascript: string, sourcemap: sm.RawSourceMap): INlsStringResult { + const { localizeCalls, nlsExpressions } = analyze(ts, typescript); if (localizeCalls.length === 0) { return { javascript, sourcemap }; @@ -438,12 +438,14 @@ module nls { } export function patchFiles(javascriptFile: File, typescript: string): File[] { + const ts = require('typescript') as typeof import('typescript'); // hack? const moduleId = javascriptFile.relative .replace(/\.js$/, '') .replace(/\\/g, '/'); const { javascript, sourcemap, nlsKeys, nls } = patch( + ts, moduleId, typescript, javascriptFile.contents.toString(), @@ -464,5 +466,3 @@ module nls { return result; } } - -export = nls; diff --git a/build/lib/node.js b/build/lib/node.js new file mode 100644 index 00000000..e727aff8 --- /dev/null +++ b/build/lib/node.js @@ -0,0 +1,17 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +const path = require("path"); +const fs = require("fs"); +const root = path.dirname(path.dirname(__dirname)); +const yarnrcPath = path.join(root, 'remote', '.yarnrc'); +const yarnrc = fs.readFileSync(yarnrcPath, 'utf8'); +const version = /^target\s+"([^"]+)"$/m.exec(yarnrc)[1]; +const platform = process.platform; +const arch = platform === 'darwin' ? 'x64' : process.arch; +const node = platform === 'win32' ? 'node.exe' : 'node'; +const nodePath = path.join(root, '.build', 'node', `v${version}`, `${platform}-${arch}`, node); +console.log(nodePath); diff --git a/build/lib/node.ts b/build/lib/node.ts index 64397034..6ac45ebb 100644 --- a/build/lib/node.ts +++ b/build/lib/node.ts @@ -10,7 +10,11 @@ const root = path.dirname(path.dirname(__dirname)); const yarnrcPath = path.join(root, 'remote', '.yarnrc'); const yarnrc = fs.readFileSync(yarnrcPath, 'utf8'); const version = /^target\s+"([^"]+)"$/m.exec(yarnrc)![1]; -const node = process.platform === 'win32' ? 'node.exe' : 'node'; -const nodePath = path.join(root, '.build', 'node', `v${version}`, `${process.platform}-${process.arch}`, node); -console.log(nodePath); \ No newline at end of file +const platform = process.platform; +const arch = platform === 'darwin' ? 'x64' : process.arch; + +const node = platform === 'win32' ? 'node.exe' : 'node'; +const nodePath = path.join(root, '.build', 'node', `v${version}`, `${platform}-${arch}`, node); + +console.log(nodePath); diff --git a/build/lib/optimize.js b/build/lib/optimize.js new file mode 100644 index 00000000..1cfca357 --- /dev/null +++ b/build/lib/optimize.js @@ -0,0 +1,202 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.minifyTask = exports.optimizeTask = exports.loaderConfig = void 0; +const es = require("event-stream"); +const gulp = require("gulp"); +const concat = require("gulp-concat"); +const filter = require("gulp-filter"); +const fancyLog = require("fancy-log"); +const ansiColors = require("ansi-colors"); +const path = require("path"); +const pump = require("pump"); +const VinylFile = require("vinyl"); +const bundle = require("./bundle"); +const i18n_1 = require("./i18n"); +const stats_1 = require("./stats"); +const util = require("./util"); +const REPO_ROOT_PATH = path.join(__dirname, '../..'); +function log(prefix, message) { + fancyLog(ansiColors.cyan('[' + prefix + ']'), message); +} +function loaderConfig() { + const result = { + paths: { + 'vs': 'out-build/vs', + 'vscode': 'empty:' + }, + amdModulesPattern: /^vs\// + }; + result['vs/css'] = { inlineResources: true }; + return result; +} +exports.loaderConfig = loaderConfig; +const IS_OUR_COPYRIGHT_REGEXP = /Copyright \(C\) Microsoft Corporation/i; +function loader(src, bundledFileHeader, bundleLoader) { + let sources = [ + `${src}/vs/loader.js` + ]; + if (bundleLoader) { + sources = sources.concat([ + `${src}/vs/css.js`, + `${src}/vs/nls.js` + ]); + } + let isFirst = true; + return (gulp + .src(sources, { base: `${src}` }) + .pipe(es.through(function (data) { + if (isFirst) { + isFirst = false; + this.emit('data', new VinylFile({ + path: 'fake', + base: '.', + contents: Buffer.from(bundledFileHeader) + })); + this.emit('data', data); + } + else { + this.emit('data', data); + } + })) + .pipe(concat('vs/loader.js'))); +} +function toConcatStream(src, bundledFileHeader, sources, dest, fileContentMapper) { + const useSourcemaps = /\.js$/.test(dest) && !/\.nls\.js$/.test(dest); + // If a bundle ends up including in any of the sources our copyright, then + // insert a fake source at the beginning of each bundle with our copyright + let containsOurCopyright = false; + for (let i = 0, len = sources.length; i < len; i++) { + const fileContents = sources[i].contents; + if (IS_OUR_COPYRIGHT_REGEXP.test(fileContents)) { + containsOurCopyright = true; + break; + } + } + if (containsOurCopyright) { + sources.unshift({ + path: null, + contents: bundledFileHeader + }); + } + const treatedSources = sources.map(function (source) { + const root = source.path ? REPO_ROOT_PATH.replace(/\\/g, '/') : ''; + const base = source.path ? root + `/${src}` : '.'; + const path = source.path ? root + '/' + source.path.replace(/\\/g, '/') : 'fake'; + const contents = source.path ? fileContentMapper(source.contents, path) : source.contents; + return new VinylFile({ + path: path, + base: base, + contents: Buffer.from(contents) + }); + }); + return es.readArray(treatedSources) + .pipe(useSourcemaps ? util.loadSourcemaps() : es.through()) + .pipe(concat(dest)) + .pipe(stats_1.createStatsStream(dest)); +} +function toBundleStream(src, bundledFileHeader, bundles, fileContentMapper) { + return es.merge(bundles.map(function (bundle) { + return toConcatStream(src, bundledFileHeader, bundle.sources, bundle.dest, fileContentMapper); + })); +} +const DEFAULT_FILE_HEADER = [ + '/*!--------------------------------------------------------', + ' * Copyright (C) Microsoft Corporation. All rights reserved.', + ' *--------------------------------------------------------*/' +].join('\n'); +function optimizeTask(opts) { + const src = opts.src; + const entryPoints = opts.entryPoints; + const resources = opts.resources; + const loaderConfig = opts.loaderConfig; + const bundledFileHeader = opts.header || DEFAULT_FILE_HEADER; + const bundleLoader = (typeof opts.bundleLoader === 'undefined' ? true : opts.bundleLoader); + const out = opts.out; + const fileContentMapper = opts.fileContentMapper || ((contents, _path) => contents); + return function () { + const sourcemaps = require('gulp-sourcemaps'); + const bundlesStream = es.through(); // this stream will contain the bundled files + const resourcesStream = es.through(); // this stream will contain the resources + const bundleInfoStream = es.through(); // this stream will contain bundleInfo.json + bundle.bundle(entryPoints, loaderConfig, function (err, result) { + if (err || !result) { + return bundlesStream.emit('error', JSON.stringify(err)); + } + toBundleStream(src, bundledFileHeader, result.files, fileContentMapper).pipe(bundlesStream); + // Remove css inlined resources + const filteredResources = resources.slice(); + result.cssInlinedResources.forEach(function (resource) { + if (process.env['VSCODE_BUILD_VERBOSE']) { + log('optimizer', 'excluding inlined: ' + resource); + } + filteredResources.push('!' + resource); + }); + gulp.src(filteredResources, { base: `${src}`, allowEmpty: true }).pipe(resourcesStream); + const bundleInfoArray = []; + if (opts.bundleInfo) { + bundleInfoArray.push(new VinylFile({ + path: 'bundleInfo.json', + base: '.', + contents: Buffer.from(JSON.stringify(result.bundleData, null, '\t')) + })); + } + es.readArray(bundleInfoArray).pipe(bundleInfoStream); + }); + const result = es.merge(loader(src, bundledFileHeader, bundleLoader), bundlesStream, resourcesStream, bundleInfoStream); + return result + .pipe(sourcemaps.write('./', { + sourceRoot: undefined, + addComment: true, + includeContent: true + })) + .pipe(opts.languages && opts.languages.length ? i18n_1.processNlsFiles({ + fileHeader: bundledFileHeader, + languages: opts.languages + }) : es.through()) + .pipe(gulp.dest(out)); + }; +} +exports.optimizeTask = optimizeTask; +function minifyTask(src, sourceMapBaseUrl) { + const esbuild = require('esbuild'); + const sourceMappingURL = sourceMapBaseUrl ? ((f) => `${sourceMapBaseUrl}/${f.relative}.map`) : undefined; + return cb => { + const cssnano = require('cssnano'); + const postcss = require('gulp-postcss'); + const sourcemaps = require('gulp-sourcemaps'); + const jsFilter = filter('**/*.js', { restore: true }); + const cssFilter = filter('**/*.css', { restore: true }); + pump(gulp.src([src + '/**', '!' + src + '/**/*.map']), jsFilter, sourcemaps.init({ loadMaps: true }), es.map((f, cb) => { + esbuild.build({ + entryPoints: [f.path], + minify: true, + sourcemap: 'external', + outdir: '.', + platform: 'node', + target: ['node12.18'], + write: false + }).then(res => { + const jsFile = res.outputFiles.find(f => /\.js$/.test(f.path)); + const sourceMapFile = res.outputFiles.find(f => /\.js\.map$/.test(f.path)); + f.contents = Buffer.from(jsFile.contents); + f.sourceMap = JSON.parse(sourceMapFile.text); + cb(undefined, f); + }, cb); + }), jsFilter.restore, cssFilter, postcss([cssnano({ preset: 'default' })]), cssFilter.restore, sourcemaps.mapSources((sourcePath) => { + if (sourcePath === 'bootstrap-fork.js') { + return 'bootstrap-fork.orig.js'; + } + return sourcePath; + }), sourcemaps.write('./', { + sourceMappingURL, + sourceRoot: undefined, + includeContent: true, + addComment: true + }), gulp.dest(src + '-min'), (err) => cb(err)); + }; +} +exports.minifyTask = minifyTask; diff --git a/build/lib/optimize.ts b/build/lib/optimize.ts index 86400889..c3c29e4a 100644 --- a/build/lib/optimize.ts +++ b/build/lib/optimize.ts @@ -8,17 +8,11 @@ import * as es from 'event-stream'; import * as gulp from 'gulp'; import * as concat from 'gulp-concat'; -import * as minifyCSS from 'gulp-cssnano'; import * as filter from 'gulp-filter'; -import * as flatmap from 'gulp-flatmap'; -import * as sourcemaps from 'gulp-sourcemaps'; -import * as uglify from 'gulp-uglify'; -import * as composer from 'gulp-uglify/composer'; import * as fancyLog from 'fancy-log'; import * as ansiColors from 'ansi-colors'; import * as path from 'path'; import * as pump from 'pump'; -import * as terser from 'terser'; import * as VinylFile from 'vinyl'; import * as bundle from './bundle'; import { Language, processNlsFiles } from './i18n'; @@ -184,6 +178,8 @@ export function optimizeTask(opts: IOptimizeTaskOpts): () => NodeJS.ReadWriteStr const fileContentMapper = opts.fileContentMapper || ((contents: string, _path: string) => contents); return function () { + const sourcemaps = require('gulp-sourcemaps') as typeof import('gulp-sourcemaps'); + const bundlesStream = es.through(); // this stream will contain the bundled files const resourcesStream = es.through(); // this stream will contain the resources const bundleInfoStream = es.through(); // this stream will contain bundleInfo.json @@ -235,62 +231,15 @@ export function optimizeTask(opts: IOptimizeTaskOpts): () => NodeJS.ReadWriteStr }; } -declare class FileWithCopyright extends VinylFile { - public __hasOurCopyright: boolean; -} -/** - * Wrap around uglify and allow the preserveComments function - * to have a file "context" to include our copyright only once per file. - */ -function uglifyWithCopyrights(): NodeJS.ReadWriteStream { - const preserveComments = (f: FileWithCopyright) => { - return (_node: any, comment: { value: string; type: string; }) => { - const text = comment.value; - const type = comment.type; - - if (/@minifier_do_not_preserve/.test(text)) { - return false; - } - - const isOurCopyright = IS_OUR_COPYRIGHT_REGEXP.test(text); - - if (isOurCopyright) { - if (f.__hasOurCopyright) { - return false; - } - f.__hasOurCopyright = true; - return true; - } - - if ('comment2' === type) { - // check for /*!. Note that text doesn't contain leading /* - return (text.length > 0 && text[0] === '!') || /@preserve|license|@cc_on|copyright/i.test(text); - } else if ('comment1' === type) { - return /license|copyright/i.test(text); - } - return false; - }; - }; - - const minify = (composer as any)(terser); - const input = es.through(); - const output = input - .pipe(flatmap((stream, f) => { - return stream.pipe(minify({ - output: { - comments: preserveComments(f), - max_line_len: 1024 - } - })); - })); - - return es.duplex(input, output); -} - export function minifyTask(src: string, sourceMapBaseUrl?: string): (cb: any) => void { + const esbuild = require('esbuild') as typeof import('esbuild'); const sourceMappingURL = sourceMapBaseUrl ? ((f: any) => `${sourceMapBaseUrl}/${f.relative}.map`) : undefined; return cb => { + const cssnano = require('cssnano') as typeof import('cssnano'); + const postcss = require('gulp-postcss') as typeof import('gulp-postcss'); + const sourcemaps = require('gulp-sourcemaps') as typeof import('gulp-sourcemaps'); + const jsFilter = filter('**/*.js', { restore: true }); const cssFilter = filter('**/*.css', { restore: true }); @@ -298,10 +247,28 @@ export function minifyTask(src: string, sourceMapBaseUrl?: string): (cb: any) => gulp.src([src + '/**', '!' + src + '/**/*.map']), jsFilter, sourcemaps.init({ loadMaps: true }), - uglifyWithCopyrights(), + es.map((f: any, cb) => { + esbuild.build({ + entryPoints: [f.path], + minify: true, + sourcemap: 'external', + outdir: '.', + platform: 'node', + target: ['node12.18'], + write: false + }).then(res => { + const jsFile = res.outputFiles.find(f => /\.js$/.test(f.path))!; + const sourceMapFile = res.outputFiles.find(f => /\.js\.map$/.test(f.path))!; + + f.contents = Buffer.from(jsFile.contents); + f.sourceMap = JSON.parse(sourceMapFile.text); + + cb(undefined, f); + }, cb); + }), jsFilter.restore, cssFilter, - minifyCSS({ reduceIdents: false }), + postcss([cssnano({ preset: 'default' })]), cssFilter.restore, (sourcemaps).mapSources((sourcePath: string) => { if (sourcePath === 'bootstrap-fork.js') { @@ -316,13 +283,7 @@ export function minifyTask(src: string, sourceMapBaseUrl?: string): (cb: any) => includeContent: true, addComment: true } as any), - gulp.dest(src + '-min') - , (err: any) => { - if (err instanceof (uglify as any).GulpUglifyError) { - console.error(`Uglify error in '${err.cause && err.cause.filename}'`); - } - - cb(err); - }); + gulp.dest(src + '-min'), + (err: any) => cb(err)); }; } diff --git a/build/lib/preLaunch.js b/build/lib/preLaunch.js new file mode 100644 index 00000000..1aecbe19 --- /dev/null +++ b/build/lib/preLaunch.js @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +// @ts-check +const path = require("path"); +const child_process_1 = require("child_process"); +const fs_1 = require("fs"); +const yarn = process.platform === 'win32' ? 'yarn.cmd' : 'yarn'; +const rootDir = path.resolve(__dirname, '..', '..'); +function runProcess(command, args = []) { + return new Promise((resolve, reject) => { + const child = child_process_1.spawn(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env }); + child.on('exit', err => !err ? resolve() : process.exit(err !== null && err !== void 0 ? err : 1)); + child.on('error', reject); + }); +} +async function exists(subdir) { + try { + await fs_1.promises.stat(path.join(rootDir, subdir)); + return true; + } + catch (_a) { + return false; + } +} +async function ensureNodeModules() { + if (!(await exists('node_modules'))) { + await runProcess(yarn); + } +} +async function getElectron() { + await runProcess(yarn, ['electron']); +} +async function ensureCompiled() { + if (!(await exists('out'))) { + await runProcess(yarn, ['compile']); + } +} +async function main() { + await ensureNodeModules(); + await getElectron(); + await ensureCompiled(); + // Can't require this until after dependencies are installed + const { getBuiltInExtensions } = require('./builtInExtensions'); + await getBuiltInExtensions(); +} +if (require.main === module) { + main().catch(err => { + console.error(err); + process.exit(1); + }); +} diff --git a/build/lib/reporter.js b/build/lib/reporter.js new file mode 100644 index 00000000..def8f24a --- /dev/null +++ b/build/lib/reporter.js @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createReporter = void 0; +const es = require("event-stream"); +const _ = require("underscore"); +const fancyLog = require("fancy-log"); +const ansiColors = require("ansi-colors"); +const fs = require("fs"); +const path = require("path"); +class ErrorLog { + constructor(id) { + this.id = id; + this.allErrors = []; + this.startTime = null; + this.count = 0; + } + onStart() { + if (this.count++ > 0) { + return; + } + this.startTime = new Date().getTime(); + fancyLog(`Starting ${ansiColors.green('compilation')}${this.id ? ansiColors.blue(` ${this.id}`) : ''}...`); + } + onEnd() { + if (--this.count > 0) { + return; + } + this.log(); + } + log() { + const errors = _.flatten(this.allErrors); + const seen = new Set(); + errors.map(err => { + if (!seen.has(err)) { + seen.add(err); + fancyLog(`${ansiColors.red('Error')}: ${err}`); + } + }); + fancyLog(`Finished ${ansiColors.green('compilation')}${this.id ? ansiColors.blue(` ${this.id}`) : ''} with ${errors.length} errors after ${ansiColors.magenta((new Date().getTime() - this.startTime) + ' ms')}`); + const regex = /^([^(]+)\((\d+),(\d+)\): (.*)$/s; + const messages = errors + .map(err => regex.exec(err)) + .filter(match => !!match) + .map(x => x) + .map(([, path, line, column, message]) => ({ path, line: parseInt(line), column: parseInt(column), message })); + try { + const logFileName = 'log' + (this.id ? `_${this.id}` : ''); + fs.writeFileSync(path.join(buildLogFolder, logFileName), JSON.stringify(messages)); + } + catch (err) { + //noop + } + } +} +const errorLogsById = new Map(); +function getErrorLog(id = '') { + let errorLog = errorLogsById.get(id); + if (!errorLog) { + errorLog = new ErrorLog(id); + errorLogsById.set(id, errorLog); + } + return errorLog; +} +const buildLogFolder = path.join(path.dirname(path.dirname(__dirname)), '.build'); +try { + fs.mkdirSync(buildLogFolder); +} +catch (err) { + // ignore +} +function createReporter(id) { + const errorLog = getErrorLog(id); + const errors = []; + errorLog.allErrors.push(errors); + const result = (err) => errors.push(err); + result.hasErrors = () => errors.length > 0; + result.end = (emitError) => { + errors.length = 0; + errorLog.onStart(); + return es.through(undefined, function () { + errorLog.onEnd(); + if (emitError && errors.length > 0) { + if (!errors.__logged__) { + errorLog.log(); + } + errors.__logged__ = true; + const err = new Error(`Found ${errors.length} errors`); + err.__reporter__ = true; + this.emit('error', err); + } + else { + this.emit('end'); + } + }); + }; + return result; +} +exports.createReporter = createReporter; diff --git a/build/lib/snapshotLoader.js b/build/lib/snapshotLoader.js new file mode 100644 index 00000000..ee626a0f --- /dev/null +++ b/build/lib/snapshotLoader.js @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +var snaps; +(function (snaps) { + const fs = require('fs'); + const path = require('path'); + const os = require('os'); + const cp = require('child_process'); + const mksnapshot = path.join(__dirname, `../../node_modules/.bin/${process.platform === 'win32' ? 'mksnapshot.cmd' : 'mksnapshot'}`); + const product = require('../../product.json'); + const arch = (process.argv.join('').match(/--arch=(.*)/) || [])[1]; + // + let loaderFilepath; + let startupBlobFilepath; + switch (process.platform) { + case 'darwin': + loaderFilepath = `VSCode-darwin/${product.nameLong}.app/Contents/Resources/app/out/vs/loader.js`; + startupBlobFilepath = `VSCode-darwin/${product.nameLong}.app/Contents/Frameworks/Electron Framework.framework/Resources/snapshot_blob.bin`; + break; + case 'win32': + case 'linux': + loaderFilepath = `VSCode-${process.platform}-${arch}/resources/app/out/vs/loader.js`; + startupBlobFilepath = `VSCode-${process.platform}-${arch}/snapshot_blob.bin`; + break; + default: + throw new Error('Unknown platform'); + } + loaderFilepath = path.join(__dirname, '../../../', loaderFilepath); + startupBlobFilepath = path.join(__dirname, '../../../', startupBlobFilepath); + snapshotLoader(loaderFilepath, startupBlobFilepath); + function snapshotLoader(loaderFilepath, startupBlobFilepath) { + const inputFile = fs.readFileSync(loaderFilepath); + const wrappedInputFile = ` + var Monaco_Loader_Init; + (function() { + var doNotInitLoader = true; + ${inputFile.toString()}; + Monaco_Loader_Init = function() { + AMDLoader.init(); + CSSLoaderPlugin.init(); + NLSLoaderPlugin.init(); + + return { define, require }; + } + })(); + `; + const wrappedInputFilepath = path.join(os.tmpdir(), 'wrapped-loader.js'); + console.log(wrappedInputFilepath); + fs.writeFileSync(wrappedInputFilepath, wrappedInputFile); + cp.execFileSync(mksnapshot, [wrappedInputFilepath, `--startup_blob`, startupBlobFilepath]); + } +})(snaps || (snaps = {})); diff --git a/build/lib/standalone.js b/build/lib/standalone.js new file mode 100644 index 00000000..91a693ce --- /dev/null +++ b/build/lib/standalone.js @@ -0,0 +1,322 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createESMSourcesAndResources2 = exports.extractEditor = void 0; +const fs = require("fs"); +const path = require("path"); +const tss = require("./treeshaking"); +const REPO_ROOT = path.join(__dirname, '../../'); +const SRC_DIR = path.join(REPO_ROOT, 'src'); +let dirCache = {}; +function writeFile(filePath, contents) { + function ensureDirs(dirPath) { + if (dirCache[dirPath]) { + return; + } + dirCache[dirPath] = true; + ensureDirs(path.dirname(dirPath)); + if (fs.existsSync(dirPath)) { + return; + } + fs.mkdirSync(dirPath); + } + ensureDirs(path.dirname(filePath)); + fs.writeFileSync(filePath, contents); +} +function extractEditor(options) { + var _a; + const ts = require('typescript'); + const tsConfig = JSON.parse(fs.readFileSync(path.join(options.sourcesRoot, 'tsconfig.monaco.json')).toString()); + let compilerOptions; + if (tsConfig.extends) { + compilerOptions = Object.assign({}, require(path.join(options.sourcesRoot, tsConfig.extends)).compilerOptions, tsConfig.compilerOptions); + delete tsConfig.extends; + } + else { + compilerOptions = tsConfig.compilerOptions; + } + tsConfig.compilerOptions = compilerOptions; + compilerOptions.noEmit = false; + compilerOptions.noUnusedLocals = false; + compilerOptions.preserveConstEnums = false; + compilerOptions.declaration = false; + compilerOptions.moduleResolution = ts.ModuleResolutionKind.Classic; + options.compilerOptions = compilerOptions; + console.log(`Running tree shaker with shakeLevel ${tss.toStringShakeLevel(options.shakeLevel)}`); + // Take the extra included .d.ts files from `tsconfig.monaco.json` + options.typings = tsConfig.include.filter(includedFile => /\.d\.ts$/.test(includedFile)); + // Add extra .d.ts files from `node_modules/@types/` + if (Array.isArray((_a = options.compilerOptions) === null || _a === void 0 ? void 0 : _a.types)) { + options.compilerOptions.types.forEach((type) => { + options.typings.push(`../node_modules/@types/${type}/index.d.ts`); + }); + } + let result = tss.shake(options); + for (let fileName in result) { + if (result.hasOwnProperty(fileName)) { + writeFile(path.join(options.destRoot, fileName), result[fileName]); + } + } + let copied = {}; + const copyFile = (fileName) => { + if (copied[fileName]) { + return; + } + copied[fileName] = true; + const srcPath = path.join(options.sourcesRoot, fileName); + const dstPath = path.join(options.destRoot, fileName); + writeFile(dstPath, fs.readFileSync(srcPath)); + }; + const writeOutputFile = (fileName, contents) => { + writeFile(path.join(options.destRoot, fileName), contents); + }; + for (let fileName in result) { + if (result.hasOwnProperty(fileName)) { + const fileContents = result[fileName]; + const info = ts.preProcessFile(fileContents); + for (let i = info.importedFiles.length - 1; i >= 0; i--) { + const importedFileName = info.importedFiles[i].fileName; + let importedFilePath; + if (/^vs\/css!/.test(importedFileName)) { + importedFilePath = importedFileName.substr('vs/css!'.length) + '.css'; + } + else { + importedFilePath = importedFileName; + } + if (/(^\.\/)|(^\.\.\/)/.test(importedFilePath)) { + importedFilePath = path.join(path.dirname(fileName), importedFilePath); + } + if (/\.css$/.test(importedFilePath)) { + transportCSS(importedFilePath, copyFile, writeOutputFile); + } + else { + if (fs.existsSync(path.join(options.sourcesRoot, importedFilePath + '.js'))) { + copyFile(importedFilePath + '.js'); + } + } + } + } + } + delete tsConfig.compilerOptions.moduleResolution; + writeOutputFile('tsconfig.json', JSON.stringify(tsConfig, null, '\t')); + [ + 'vs/css.build.js', + 'vs/css.d.ts', + 'vs/css.js', + 'vs/loader.js', + 'vs/nls.build.js', + 'vs/nls.d.ts', + 'vs/nls.js', + 'vs/nls.mock.ts', + ].forEach(copyFile); +} +exports.extractEditor = extractEditor; +function createESMSourcesAndResources2(options) { + const ts = require('typescript'); + const SRC_FOLDER = path.join(REPO_ROOT, options.srcFolder); + const OUT_FOLDER = path.join(REPO_ROOT, options.outFolder); + const OUT_RESOURCES_FOLDER = path.join(REPO_ROOT, options.outResourcesFolder); + const getDestAbsoluteFilePath = (file) => { + let dest = options.renames[file.replace(/\\/g, '/')] || file; + if (dest === 'tsconfig.json') { + return path.join(OUT_FOLDER, `tsconfig.json`); + } + if (/\.ts$/.test(dest)) { + return path.join(OUT_FOLDER, dest); + } + return path.join(OUT_RESOURCES_FOLDER, dest); + }; + const allFiles = walkDirRecursive(SRC_FOLDER); + for (const file of allFiles) { + if (options.ignores.indexOf(file.replace(/\\/g, '/')) >= 0) { + continue; + } + if (file === 'tsconfig.json') { + const tsConfig = JSON.parse(fs.readFileSync(path.join(SRC_FOLDER, file)).toString()); + tsConfig.compilerOptions.module = 'es6'; + tsConfig.compilerOptions.outDir = path.join(path.relative(OUT_FOLDER, OUT_RESOURCES_FOLDER), 'vs').replace(/\\/g, '/'); + write(getDestAbsoluteFilePath(file), JSON.stringify(tsConfig, null, '\t')); + continue; + } + if (/\.d\.ts$/.test(file) || /\.css$/.test(file) || /\.js$/.test(file) || /\.ttf$/.test(file)) { + // Transport the files directly + write(getDestAbsoluteFilePath(file), fs.readFileSync(path.join(SRC_FOLDER, file))); + continue; + } + if (/\.ts$/.test(file)) { + // Transform the .ts file + let fileContents = fs.readFileSync(path.join(SRC_FOLDER, file)).toString(); + const info = ts.preProcessFile(fileContents); + for (let i = info.importedFiles.length - 1; i >= 0; i--) { + const importedFilename = info.importedFiles[i].fileName; + const pos = info.importedFiles[i].pos; + const end = info.importedFiles[i].end; + let importedFilepath; + if (/^vs\/css!/.test(importedFilename)) { + importedFilepath = importedFilename.substr('vs/css!'.length) + '.css'; + } + else { + importedFilepath = importedFilename; + } + if (/(^\.\/)|(^\.\.\/)/.test(importedFilepath)) { + importedFilepath = path.join(path.dirname(file), importedFilepath); + } + let relativePath; + if (importedFilepath === path.dirname(file).replace(/\\/g, '/')) { + relativePath = '../' + path.basename(path.dirname(file)); + } + else if (importedFilepath === path.dirname(path.dirname(file)).replace(/\\/g, '/')) { + relativePath = '../../' + path.basename(path.dirname(path.dirname(file))); + } + else { + relativePath = path.relative(path.dirname(file), importedFilepath); + } + relativePath = relativePath.replace(/\\/g, '/'); + if (!/(^\.\/)|(^\.\.\/)/.test(relativePath)) { + relativePath = './' + relativePath; + } + fileContents = (fileContents.substring(0, pos + 1) + + relativePath + + fileContents.substring(end + 1)); + } + fileContents = fileContents.replace(/import ([a-zA-z0-9]+) = require\(('[^']+')\);/g, function (_, m1, m2) { + return `import * as ${m1} from ${m2};`; + }); + write(getDestAbsoluteFilePath(file), fileContents); + continue; + } + console.log(`UNKNOWN FILE: ${file}`); + } + function walkDirRecursive(dir) { + if (dir.charAt(dir.length - 1) !== '/' || dir.charAt(dir.length - 1) !== '\\') { + dir += '/'; + } + let result = []; + _walkDirRecursive(dir, result, dir.length); + return result; + } + function _walkDirRecursive(dir, result, trimPos) { + const files = fs.readdirSync(dir); + for (let i = 0; i < files.length; i++) { + const file = path.join(dir, files[i]); + if (fs.statSync(file).isDirectory()) { + _walkDirRecursive(file, result, trimPos); + } + else { + result.push(file.substr(trimPos)); + } + } + } + function write(absoluteFilePath, contents) { + if (/(\.ts$)|(\.js$)/.test(absoluteFilePath)) { + contents = toggleComments(contents.toString()); + } + writeFile(absoluteFilePath, contents); + function toggleComments(fileContents) { + let lines = fileContents.split(/\r\n|\r|\n/); + let mode = 0; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (mode === 0) { + if (/\/\/ ESM-comment-begin/.test(line)) { + mode = 1; + continue; + } + if (/\/\/ ESM-uncomment-begin/.test(line)) { + mode = 2; + continue; + } + continue; + } + if (mode === 1) { + if (/\/\/ ESM-comment-end/.test(line)) { + mode = 0; + continue; + } + lines[i] = '// ' + line; + continue; + } + if (mode === 2) { + if (/\/\/ ESM-uncomment-end/.test(line)) { + mode = 0; + continue; + } + lines[i] = line.replace(/^(\s*)\/\/ ?/, function (_, indent) { + return indent; + }); + } + } + return lines.join('\n'); + } + } +} +exports.createESMSourcesAndResources2 = createESMSourcesAndResources2; +function transportCSS(module, enqueue, write) { + if (!/\.css/.test(module)) { + return false; + } + const filename = path.join(SRC_DIR, module); + const fileContents = fs.readFileSync(filename).toString(); + const inlineResources = 'base64'; // see https://github.com/microsoft/monaco-editor/issues/148 + const newContents = _rewriteOrInlineUrls(fileContents, inlineResources === 'base64'); + write(module, newContents); + return true; + function _rewriteOrInlineUrls(contents, forceBase64) { + return _replaceURL(contents, (url) => { + const fontMatch = url.match(/^(.*).ttf\?(.*)$/); + if (fontMatch) { + const relativeFontPath = `${fontMatch[1]}.ttf`; // trim the query parameter + const fontPath = path.join(path.dirname(module), relativeFontPath); + enqueue(fontPath); + return relativeFontPath; + } + const imagePath = path.join(path.dirname(module), url); + const fileContents = fs.readFileSync(path.join(SRC_DIR, imagePath)); + const MIME = /\.svg$/.test(url) ? 'image/svg+xml' : 'image/png'; + let DATA = ';base64,' + fileContents.toString('base64'); + if (!forceBase64 && /\.svg$/.test(url)) { + // .svg => url encode as explained at https://codepen.io/tigt/post/optimizing-svgs-in-data-uris + let newText = fileContents.toString() + .replace(/"/g, '\'') + .replace(//g, '%3E') + .replace(/&/g, '%26') + .replace(/#/g, '%23') + .replace(/\s+/g, ' '); + let encodedData = ',' + newText; + if (encodedData.length < DATA.length) { + DATA = encodedData; + } + } + return '"data:' + MIME + DATA + '"'; + }); + } + function _replaceURL(contents, replacer) { + // Use ")" as the terminator as quotes are oftentimes not used at all + return contents.replace(/url\(\s*([^\)]+)\s*\)?/g, (_, ...matches) => { + let url = matches[0]; + // Eliminate starting quotes (the initial whitespace is not captured) + if (url.charAt(0) === '"' || url.charAt(0) === '\'') { + url = url.substring(1); + } + // The ending whitespace is captured + while (url.length > 0 && (url.charAt(url.length - 1) === ' ' || url.charAt(url.length - 1) === '\t')) { + url = url.substring(0, url.length - 1); + } + // Eliminate ending quotes + if (url.charAt(url.length - 1) === '"' || url.charAt(url.length - 1) === '\'') { + url = url.substring(0, url.length - 1); + } + if (!_startsWith(url, 'data:') && !_startsWith(url, 'http://') && !_startsWith(url, 'https://')) { + url = replacer(url); + } + return 'url(' + url + ')'; + }); + } + function _startsWith(haystack, needle) { + return haystack.length >= needle.length && haystack.substr(0, needle.length) === needle; + } +} diff --git a/build/lib/standalone.ts b/build/lib/standalone.ts index 51868c22..8b87b9fa 100644 --- a/build/lib/standalone.ts +++ b/build/lib/standalone.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as ts from 'typescript'; import * as fs from 'fs'; import * as path from 'path'; import * as tss from './treeshaking'; @@ -31,6 +30,8 @@ function writeFile(filePath: string, contents: Buffer | string): void { } export function extractEditor(options: tss.ITreeShakingOptions & { destRoot: string }): void { + const ts = require('typescript') as typeof import('typescript'); + const tsConfig = JSON.parse(fs.readFileSync(path.join(options.sourcesRoot, 'tsconfig.monaco.json')).toString()); let compilerOptions: { [key: string]: any }; if (tsConfig.extends) { @@ -134,6 +135,8 @@ export interface IOptions2 { } export function createESMSourcesAndResources2(options: IOptions2): void { + const ts = require('typescript') as typeof import('typescript'); + const SRC_FOLDER = path.join(REPO_ROOT, options.srcFolder); const OUT_FOLDER = path.join(REPO_ROOT, options.outFolder); const OUT_RESOURCES_FOLDER = path.join(REPO_ROOT, options.outResourcesFolder); diff --git a/build/lib/stats.js b/build/lib/stats.js new file mode 100644 index 00000000..9a239e88 --- /dev/null +++ b/build/lib/stats.js @@ -0,0 +1,137 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.submitAllStats = exports.createStatsStream = void 0; +const es = require("event-stream"); +const fancyLog = require("fancy-log"); +const ansiColors = require("ansi-colors"); +class Entry { + constructor(name, totalCount, totalSize) { + this.name = name; + this.totalCount = totalCount; + this.totalSize = totalSize; + } + toString(pretty) { + if (!pretty) { + if (this.totalCount === 1) { + return `${this.name}: ${this.totalSize} bytes`; + } + else { + return `${this.name}: ${this.totalCount} files with ${this.totalSize} bytes`; + } + } + else { + if (this.totalCount === 1) { + return `Stats for '${ansiColors.grey(this.name)}': ${Math.round(this.totalSize / 1204)}KB`; + } + else { + const count = this.totalCount < 100 + ? ansiColors.green(this.totalCount.toString()) + : ansiColors.red(this.totalCount.toString()); + return `Stats for '${ansiColors.grey(this.name)}': ${count} files, ${Math.round(this.totalSize / 1204)}KB`; + } + } + } +} +const _entries = new Map(); +function createStatsStream(group, log) { + const entry = new Entry(group, 0, 0); + _entries.set(entry.name, entry); + return es.through(function (data) { + const file = data; + if (typeof file.path === 'string') { + entry.totalCount += 1; + if (Buffer.isBuffer(file.contents)) { + entry.totalSize += file.contents.length; + } + else if (file.stat && typeof file.stat.size === 'number') { + entry.totalSize += file.stat.size; + } + else { + // funky file... + } + } + this.emit('data', data); + }, function () { + if (log) { + if (entry.totalCount === 1) { + fancyLog(`Stats for '${ansiColors.grey(entry.name)}': ${Math.round(entry.totalSize / 1204)}KB`); + } + else { + const count = entry.totalCount < 100 + ? ansiColors.green(entry.totalCount.toString()) + : ansiColors.red(entry.totalCount.toString()); + fancyLog(`Stats for '${ansiColors.grey(entry.name)}': ${count} files, ${Math.round(entry.totalSize / 1204)}KB`); + } + } + this.emit('end'); + }); +} +exports.createStatsStream = createStatsStream; +function submitAllStats(productJson, commit) { + const appInsights = require('applicationinsights'); + const sorted = []; + // move entries for single files to the front + _entries.forEach(value => { + if (value.totalCount === 1) { + sorted.unshift(value); + } + else { + sorted.push(value); + } + }); + // print to console + for (const entry of sorted) { + console.log(entry.toString(true)); + } + // send data as telementry event when the + // product is configured to send telemetry + if (!productJson || !productJson.aiConfig || typeof productJson.aiConfig.asimovKey !== 'string') { + return Promise.resolve(false); + } + return new Promise(resolve => { + try { + const sizes = {}; + const counts = {}; + for (const entry of sorted) { + sizes[entry.name] = entry.totalSize; + counts[entry.name] = entry.totalCount; + } + appInsights.setup(productJson.aiConfig.asimovKey) + .setAutoCollectConsole(false) + .setAutoCollectExceptions(false) + .setAutoCollectPerformance(false) + .setAutoCollectRequests(false) + .setAutoCollectDependencies(false) + .setAutoDependencyCorrelation(false) + .start(); + appInsights.defaultClient.config.endpointUrl = 'https://vortex.data.microsoft.com/collect/v1'; + /* __GDPR__ + "monacoworkbench/packagemetrics" : { + "commit" : {"classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "size" : {"classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "count" : {"classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } + */ + appInsights.defaultClient.trackEvent({ + name: 'monacoworkbench/packagemetrics', + properties: { commit, size: JSON.stringify(sizes), count: JSON.stringify(counts) } + }); + appInsights.defaultClient.flush({ + callback: () => { + appInsights.dispose(); + resolve(true); + } + }); + } + catch (err) { + console.error('ERROR sending build stats as telemetry event!'); + console.error(err); + resolve(false); + } + }); +} +exports.submitAllStats = submitAllStats; diff --git a/build/lib/stats.ts b/build/lib/stats.ts index a94b1c9a..ce141a2f 100644 --- a/build/lib/stats.ts +++ b/build/lib/stats.ts @@ -9,7 +9,6 @@ import * as es from 'event-stream'; import * as fancyLog from 'fancy-log'; import * as ansiColors from 'ansi-colors'; import * as File from 'vinyl'; -import * as appInsights from 'applicationinsights'; class Entry { constructor(readonly name: string, public totalCount: number, public totalSize: number) { } @@ -75,6 +74,7 @@ export function createStatsStream(group: string, log?: boolean): es.ThroughStrea } export function submitAllStats(productJson: any, commit: string): Promise { + const appInsights = require('applicationinsights') as typeof import('applicationinsights'); const sorted: Entry[] = []; // move entries for single files to the front diff --git a/build/lib/task.js b/build/lib/task.js new file mode 100644 index 00000000..d08ab8ac --- /dev/null +++ b/build/lib/task.js @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.define = exports.parallel = exports.series = void 0; +const fancyLog = require("fancy-log"); +const ansiColors = require("ansi-colors"); +function _isPromise(p) { + if (typeof p.then === 'function') { + return true; + } + return false; +} +function _renderTime(time) { + return `${Math.round(time)} ms`; +} +async function _execute(task) { + const name = task.taskName || task.displayName || ``; + if (!task._tasks) { + fancyLog('Starting', ansiColors.cyan(name), '...'); + } + const startTime = process.hrtime(); + await _doExecute(task); + const elapsedArr = process.hrtime(startTime); + const elapsedNanoseconds = (elapsedArr[0] * 1e9 + elapsedArr[1]); + if (!task._tasks) { + fancyLog(`Finished`, ansiColors.cyan(name), 'after', ansiColors.magenta(_renderTime(elapsedNanoseconds / 1e6))); + } +} +async function _doExecute(task) { + // Always invoke as if it were a callback task + return new Promise((resolve, reject) => { + if (task.length === 1) { + // this is a callback task + task((err) => { + if (err) { + return reject(err); + } + resolve(); + }); + return; + } + const taskResult = task(); + if (typeof taskResult === 'undefined') { + // this is a sync task + resolve(); + return; + } + if (_isPromise(taskResult)) { + // this is a promise returning task + taskResult.then(resolve, reject); + return; + } + // this is a stream returning task + taskResult.on('end', _ => resolve()); + taskResult.on('error', err => reject(err)); + }); +} +function series(...tasks) { + const result = async () => { + for (let i = 0; i < tasks.length; i++) { + await _execute(tasks[i]); + } + }; + result._tasks = tasks; + return result; +} +exports.series = series; +function parallel(...tasks) { + const result = async () => { + await Promise.all(tasks.map(t => _execute(t))); + }; + result._tasks = tasks; + return result; +} +exports.parallel = parallel; +function define(name, task) { + if (task._tasks) { + // This is a composite task + const lastTask = task._tasks[task._tasks.length - 1]; + if (lastTask._tasks || lastTask.taskName) { + // This is a composite task without a real task function + // => generate a fake task function + return define(name, series(task, () => Promise.resolve())); + } + lastTask.taskName = name; + task.displayName = name; + return task; + } + // This is a simple task + task.taskName = name; + task.displayName = name; + return task; +} +exports.define = define; diff --git a/build/lib/test/i18n.test.js b/build/lib/test/i18n.test.js new file mode 100644 index 00000000..3dd10425 --- /dev/null +++ b/build/lib/test/i18n.test.js @@ -0,0 +1,40 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +const assert = require("assert"); +const i18n = require("../i18n"); +suite('XLF Parser Tests', () => { + const sampleXlf = 'Key #1Key #2 &'; + const sampleTranslatedXlf = 'Key #1Кнопка #1Key #2 &Кнопка #2 &'; + const originalFilePath = 'vs/base/common/keybinding'; + const keys = ['key1', 'key2']; + const messages = ['Key #1', 'Key #2 &']; + const translatedMessages = { key1: 'Кнопка #1', key2: 'Кнопка #2 &' }; + test('Keys & messages to XLF conversion', () => { + const xlf = new i18n.XLF('vscode-workbench'); + xlf.addFile(originalFilePath, keys, messages); + const xlfString = xlf.toString(); + assert.strictEqual(xlfString.replace(/\s{2,}/g, ''), sampleXlf); + }); + test('XLF to keys & messages conversion', () => { + i18n.XLF.parse(sampleTranslatedXlf).then(function (resolvedFiles) { + assert.deepEqual(resolvedFiles[0].messages, translatedMessages); + assert.strictEqual(resolvedFiles[0].originalFilePath, originalFilePath); + }); + }); + test('JSON file source path to Transifex resource match', () => { + const editorProject = 'vscode-editor', workbenchProject = 'vscode-workbench'; + const platform = { name: 'vs/platform', project: editorProject }, editorContrib = { name: 'vs/editor/contrib', project: editorProject }, editor = { name: 'vs/editor', project: editorProject }, base = { name: 'vs/base', project: editorProject }, code = { name: 'vs/code', project: workbenchProject }, workbenchParts = { name: 'vs/workbench/contrib/html', project: workbenchProject }, workbenchServices = { name: 'vs/workbench/services/textfile', project: workbenchProject }, workbench = { name: 'vs/workbench', project: workbenchProject }; + assert.deepEqual(i18n.getResource('vs/platform/actions/browser/menusExtensionPoint'), platform); + assert.deepEqual(i18n.getResource('vs/editor/contrib/clipboard/browser/clipboard'), editorContrib); + assert.deepEqual(i18n.getResource('vs/editor/common/modes/modesRegistry'), editor); + assert.deepEqual(i18n.getResource('vs/base/common/errorMessage'), base); + assert.deepEqual(i18n.getResource('vs/code/electron-main/window'), code); + assert.deepEqual(i18n.getResource('vs/workbench/contrib/html/browser/webview'), workbenchParts); + assert.deepEqual(i18n.getResource('vs/workbench/services/textfile/node/testFileService'), workbenchServices); + assert.deepEqual(i18n.getResource('vs/workbench/browser/parts/panel/panelActions'), workbench); + }); +}); diff --git a/build/lib/treeshaking.js b/build/lib/treeshaking.js new file mode 100644 index 00000000..19c45a10 --- /dev/null +++ b/build/lib/treeshaking.js @@ -0,0 +1,780 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.shake = exports.toStringShakeLevel = exports.ShakeLevel = void 0; +const fs = require("fs"); +const path = require("path"); +const TYPESCRIPT_LIB_FOLDER = path.dirname(require.resolve('typescript/lib/lib.d.ts')); +var ShakeLevel; +(function (ShakeLevel) { + ShakeLevel[ShakeLevel["Files"] = 0] = "Files"; + ShakeLevel[ShakeLevel["InnerFile"] = 1] = "InnerFile"; + ShakeLevel[ShakeLevel["ClassMembers"] = 2] = "ClassMembers"; +})(ShakeLevel = exports.ShakeLevel || (exports.ShakeLevel = {})); +function toStringShakeLevel(shakeLevel) { + switch (shakeLevel) { + case 0 /* Files */: + return 'Files (0)'; + case 1 /* InnerFile */: + return 'InnerFile (1)'; + case 2 /* ClassMembers */: + return 'ClassMembers (2)'; + } +} +exports.toStringShakeLevel = toStringShakeLevel; +function printDiagnostics(options, diagnostics) { + for (const diag of diagnostics) { + let result = ''; + if (diag.file) { + result += `${path.join(options.sourcesRoot, diag.file.fileName)}`; + } + if (diag.file && diag.start) { + let location = diag.file.getLineAndCharacterOfPosition(diag.start); + result += `:${location.line + 1}:${location.character}`; + } + result += ` - ` + JSON.stringify(diag.messageText); + console.log(result); + } +} +function shake(options) { + const ts = require('typescript'); + const languageService = createTypeScriptLanguageService(ts, options); + const program = languageService.getProgram(); + const globalDiagnostics = program.getGlobalDiagnostics(); + if (globalDiagnostics.length > 0) { + printDiagnostics(options, globalDiagnostics); + throw new Error(`Compilation Errors encountered.`); + } + const syntacticDiagnostics = program.getSyntacticDiagnostics(); + if (syntacticDiagnostics.length > 0) { + printDiagnostics(options, syntacticDiagnostics); + throw new Error(`Compilation Errors encountered.`); + } + const semanticDiagnostics = program.getSemanticDiagnostics(); + if (semanticDiagnostics.length > 0) { + printDiagnostics(options, semanticDiagnostics); + throw new Error(`Compilation Errors encountered.`); + } + markNodes(ts, languageService, options); + return generateResult(ts, languageService, options.shakeLevel); +} +exports.shake = shake; +//#region Discovery, LanguageService & Setup +function createTypeScriptLanguageService(ts, options) { + // Discover referenced files + const FILES = discoverAndReadFiles(ts, options); + // Add fake usage files + options.inlineEntryPoints.forEach((inlineEntryPoint, index) => { + FILES[`inlineEntryPoint.${index}.ts`] = inlineEntryPoint; + }); + // Add additional typings + options.typings.forEach((typing) => { + const filePath = path.join(options.sourcesRoot, typing); + FILES[typing] = fs.readFileSync(filePath).toString(); + }); + // Resolve libs + const RESOLVED_LIBS = processLibFiles(ts, options); + const compilerOptions = ts.convertCompilerOptionsFromJson(options.compilerOptions, options.sourcesRoot).options; + const host = new TypeScriptLanguageServiceHost(ts, RESOLVED_LIBS, FILES, compilerOptions); + return ts.createLanguageService(host); +} +/** + * Read imports and follow them until all files have been handled + */ +function discoverAndReadFiles(ts, options) { + const FILES = {}; + const in_queue = Object.create(null); + const queue = []; + const enqueue = (moduleId) => { + if (in_queue[moduleId]) { + return; + } + in_queue[moduleId] = true; + queue.push(moduleId); + }; + options.entryPoints.forEach((entryPoint) => enqueue(entryPoint)); + while (queue.length > 0) { + const moduleId = queue.shift(); + const dts_filename = path.join(options.sourcesRoot, moduleId + '.d.ts'); + if (fs.existsSync(dts_filename)) { + const dts_filecontents = fs.readFileSync(dts_filename).toString(); + FILES[`${moduleId}.d.ts`] = dts_filecontents; + continue; + } + const js_filename = path.join(options.sourcesRoot, moduleId + '.js'); + if (fs.existsSync(js_filename)) { + // This is an import for a .js file, so ignore it... + continue; + } + let ts_filename; + if (options.redirects[moduleId]) { + ts_filename = path.join(options.sourcesRoot, options.redirects[moduleId] + '.ts'); + } + else { + ts_filename = path.join(options.sourcesRoot, moduleId + '.ts'); + } + const ts_filecontents = fs.readFileSync(ts_filename).toString(); + const info = ts.preProcessFile(ts_filecontents); + for (let i = info.importedFiles.length - 1; i >= 0; i--) { + const importedFileName = info.importedFiles[i].fileName; + if (options.importIgnorePattern.test(importedFileName)) { + // Ignore vs/css! imports + continue; + } + let importedModuleId = importedFileName; + if (/(^\.\/)|(^\.\.\/)/.test(importedModuleId)) { + importedModuleId = path.join(path.dirname(moduleId), importedModuleId); + } + enqueue(importedModuleId); + } + FILES[`${moduleId}.ts`] = ts_filecontents; + } + return FILES; +} +/** + * Read lib files and follow lib references + */ +function processLibFiles(ts, options) { + const stack = [...options.compilerOptions.lib]; + const result = {}; + while (stack.length > 0) { + const filename = `lib.${stack.shift().toLowerCase()}.d.ts`; + const key = `defaultLib:${filename}`; + if (!result[key]) { + // add this file + const filepath = path.join(TYPESCRIPT_LIB_FOLDER, filename); + const sourceText = fs.readFileSync(filepath).toString(); + result[key] = sourceText; + // precess dependencies and "recurse" + const info = ts.preProcessFile(sourceText); + for (let ref of info.libReferenceDirectives) { + stack.push(ref.fileName); + } + } + } + return result; +} +/** + * A TypeScript language service host + */ +class TypeScriptLanguageServiceHost { + constructor(ts, libs, files, compilerOptions) { + this._ts = ts; + this._libs = libs; + this._files = files; + this._compilerOptions = compilerOptions; + } + // --- language service host --------------- + getCompilationSettings() { + return this._compilerOptions; + } + getScriptFileNames() { + return ([] + .concat(Object.keys(this._libs)) + .concat(Object.keys(this._files))); + } + getScriptVersion(_fileName) { + return '1'; + } + getProjectVersion() { + return '1'; + } + getScriptSnapshot(fileName) { + if (this._files.hasOwnProperty(fileName)) { + return this._ts.ScriptSnapshot.fromString(this._files[fileName]); + } + else if (this._libs.hasOwnProperty(fileName)) { + return this._ts.ScriptSnapshot.fromString(this._libs[fileName]); + } + else { + return this._ts.ScriptSnapshot.fromString(''); + } + } + getScriptKind(_fileName) { + return this._ts.ScriptKind.TS; + } + getCurrentDirectory() { + return ''; + } + getDefaultLibFileName(_options) { + return 'defaultLib:lib.d.ts'; + } + isDefaultLibFileName(fileName) { + return fileName === this.getDefaultLibFileName(this._compilerOptions); + } +} +//#endregion +//#region Tree Shaking +var NodeColor; +(function (NodeColor) { + NodeColor[NodeColor["White"] = 0] = "White"; + NodeColor[NodeColor["Gray"] = 1] = "Gray"; + NodeColor[NodeColor["Black"] = 2] = "Black"; +})(NodeColor || (NodeColor = {})); +function getColor(node) { + return node.$$$color || 0 /* White */; +} +function setColor(node, color) { + node.$$$color = color; +} +function nodeOrParentIsBlack(node) { + while (node) { + const color = getColor(node); + if (color === 2 /* Black */) { + return true; + } + node = node.parent; + } + return false; +} +function nodeOrChildIsBlack(node) { + if (getColor(node) === 2 /* Black */) { + return true; + } + for (const child of node.getChildren()) { + if (nodeOrChildIsBlack(child)) { + return true; + } + } + return false; +} +function markNodes(ts, languageService, options) { + const program = languageService.getProgram(); + if (!program) { + throw new Error('Could not get program from language service'); + } + if (options.shakeLevel === 0 /* Files */) { + // Mark all source files Black + program.getSourceFiles().forEach((sourceFile) => { + setColor(sourceFile, 2 /* Black */); + }); + return; + } + const black_queue = []; + const gray_queue = []; + const export_import_queue = []; + const sourceFilesLoaded = {}; + function enqueueTopLevelModuleStatements(sourceFile) { + sourceFile.forEachChild((node) => { + if (ts.isImportDeclaration(node)) { + if (!node.importClause && ts.isStringLiteral(node.moduleSpecifier)) { + setColor(node, 2 /* Black */); + enqueueImport(node, node.moduleSpecifier.text); + } + return; + } + if (ts.isExportDeclaration(node)) { + if (!node.exportClause && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) { + // export * from "foo"; + setColor(node, 2 /* Black */); + enqueueImport(node, node.moduleSpecifier.text); + } + if (node.exportClause && ts.isNamedExports(node.exportClause)) { + for (const exportSpecifier of node.exportClause.elements) { + export_import_queue.push(exportSpecifier); + } + } + return; + } + if (ts.isExpressionStatement(node) + || ts.isIfStatement(node) + || ts.isIterationStatement(node, true) + || ts.isExportAssignment(node)) { + enqueue_black(node); + } + if (ts.isImportEqualsDeclaration(node)) { + if (/export/.test(node.getFullText(sourceFile))) { + // e.g. "export import Severity = BaseSeverity;" + enqueue_black(node); + } + } + }); + } + function enqueue_gray(node) { + if (nodeOrParentIsBlack(node) || getColor(node) === 1 /* Gray */) { + return; + } + setColor(node, 1 /* Gray */); + gray_queue.push(node); + } + function enqueue_black(node) { + const previousColor = getColor(node); + if (previousColor === 2 /* Black */) { + return; + } + if (previousColor === 1 /* Gray */) { + // remove from gray queue + gray_queue.splice(gray_queue.indexOf(node), 1); + setColor(node, 0 /* White */); + // add to black queue + enqueue_black(node); + // // move from one queue to the other + // black_queue.push(node); + // setColor(node, NodeColor.Black); + return; + } + if (nodeOrParentIsBlack(node)) { + return; + } + const fileName = node.getSourceFile().fileName; + if (/^defaultLib:/.test(fileName) || /\.d\.ts$/.test(fileName)) { + setColor(node, 2 /* Black */); + return; + } + const sourceFile = node.getSourceFile(); + if (!sourceFilesLoaded[sourceFile.fileName]) { + sourceFilesLoaded[sourceFile.fileName] = true; + enqueueTopLevelModuleStatements(sourceFile); + } + if (ts.isSourceFile(node)) { + return; + } + setColor(node, 2 /* Black */); + black_queue.push(node); + if (options.shakeLevel === 2 /* ClassMembers */ && (ts.isMethodDeclaration(node) || ts.isMethodSignature(node) || ts.isPropertySignature(node) || ts.isPropertyDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node))) { + const references = languageService.getReferencesAtPosition(node.getSourceFile().fileName, node.name.pos + node.name.getLeadingTriviaWidth()); + if (references) { + for (let i = 0, len = references.length; i < len; i++) { + const reference = references[i]; + const referenceSourceFile = program.getSourceFile(reference.fileName); + if (!referenceSourceFile) { + continue; + } + const referenceNode = getTokenAtPosition(ts, referenceSourceFile, reference.textSpan.start, false, false); + if (ts.isMethodDeclaration(referenceNode.parent) + || ts.isPropertyDeclaration(referenceNode.parent) + || ts.isGetAccessor(referenceNode.parent) + || ts.isSetAccessor(referenceNode.parent)) { + enqueue_gray(referenceNode.parent); + } + } + } + } + } + function enqueueFile(filename) { + const sourceFile = program.getSourceFile(filename); + if (!sourceFile) { + console.warn(`Cannot find source file ${filename}`); + return; + } + enqueue_black(sourceFile); + } + function enqueueImport(node, importText) { + if (options.importIgnorePattern.test(importText)) { + // this import should be ignored + return; + } + const nodeSourceFile = node.getSourceFile(); + let fullPath; + if (/(^\.\/)|(^\.\.\/)/.test(importText)) { + fullPath = path.join(path.dirname(nodeSourceFile.fileName), importText) + '.ts'; + } + else { + fullPath = importText + '.ts'; + } + enqueueFile(fullPath); + } + options.entryPoints.forEach(moduleId => enqueueFile(moduleId + '.ts')); + // Add fake usage files + options.inlineEntryPoints.forEach((_, index) => enqueueFile(`inlineEntryPoint.${index}.ts`)); + let step = 0; + const checker = program.getTypeChecker(); + while (black_queue.length > 0 || gray_queue.length > 0) { + ++step; + let node; + if (step % 100 === 0) { + console.log(`Treeshaking - ${Math.floor(100 * step / (step + black_queue.length + gray_queue.length))}% - ${step}/${step + black_queue.length + gray_queue.length} (${black_queue.length}, ${gray_queue.length})`); + } + if (black_queue.length === 0) { + for (let i = 0; i < gray_queue.length; i++) { + const node = gray_queue[i]; + const nodeParent = node.parent; + if ((ts.isClassDeclaration(nodeParent) || ts.isInterfaceDeclaration(nodeParent)) && nodeOrChildIsBlack(nodeParent)) { + gray_queue.splice(i, 1); + black_queue.push(node); + setColor(node, 2 /* Black */); + i--; + } + } + } + if (black_queue.length > 0) { + node = black_queue.shift(); + } + else { + // only gray nodes remaining... + break; + } + const nodeSourceFile = node.getSourceFile(); + const loop = (node) => { + const [symbol, symbolImportNode] = getRealNodeSymbol(ts, checker, node); + if (symbolImportNode) { + setColor(symbolImportNode, 2 /* Black */); + } + if (symbol && !nodeIsInItsOwnDeclaration(nodeSourceFile, node, symbol)) { + for (let i = 0, len = symbol.declarations.length; i < len; i++) { + const declaration = symbol.declarations[i]; + if (ts.isSourceFile(declaration)) { + // Do not enqueue full source files + // (they can be the declaration of a module import) + continue; + } + if (options.shakeLevel === 2 /* ClassMembers */ && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && !isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(ts, program, checker, declaration)) { + enqueue_black(declaration.name); + for (let j = 0; j < declaration.members.length; j++) { + const member = declaration.members[j]; + const memberName = member.name ? member.name.getText() : null; + if (ts.isConstructorDeclaration(member) + || ts.isConstructSignatureDeclaration(member) + || ts.isIndexSignatureDeclaration(member) + || ts.isCallSignatureDeclaration(member) + || memberName === '[Symbol.iterator]' + || memberName === '[Symbol.toStringTag]' + || memberName === 'toJSON' + || memberName === 'toString' + || memberName === 'dispose' // TODO: keeping all `dispose` methods + || /^_(.*)Brand$/.test(memberName || '') // TODO: keeping all members ending with `Brand`... + ) { + enqueue_black(member); + } + } + // queue the heritage clauses + if (declaration.heritageClauses) { + for (let heritageClause of declaration.heritageClauses) { + enqueue_black(heritageClause); + } + } + } + else { + enqueue_black(declaration); + } + } + } + node.forEachChild(loop); + }; + node.forEachChild(loop); + } + while (export_import_queue.length > 0) { + const node = export_import_queue.shift(); + if (nodeOrParentIsBlack(node)) { + continue; + } + const symbol = node.symbol; + if (!symbol) { + continue; + } + const aliased = checker.getAliasedSymbol(symbol); + if (aliased.declarations && aliased.declarations.length > 0) { + if (nodeOrParentIsBlack(aliased.declarations[0]) || nodeOrChildIsBlack(aliased.declarations[0])) { + setColor(node, 2 /* Black */); + } + } + } +} +function nodeIsInItsOwnDeclaration(nodeSourceFile, node, symbol) { + for (let i = 0, len = symbol.declarations.length; i < len; i++) { + const declaration = symbol.declarations[i]; + const declarationSourceFile = declaration.getSourceFile(); + if (nodeSourceFile === declarationSourceFile) { + if (declaration.pos <= node.pos && node.end <= declaration.end) { + return true; + } + } + } + return false; +} +function generateResult(ts, languageService, shakeLevel) { + const program = languageService.getProgram(); + if (!program) { + throw new Error('Could not get program from language service'); + } + let result = {}; + const writeFile = (filePath, contents) => { + result[filePath] = contents; + }; + program.getSourceFiles().forEach((sourceFile) => { + const fileName = sourceFile.fileName; + if (/^defaultLib:/.test(fileName)) { + return; + } + const destination = fileName; + if (/\.d\.ts$/.test(fileName)) { + if (nodeOrChildIsBlack(sourceFile)) { + writeFile(destination, sourceFile.text); + } + return; + } + let text = sourceFile.text; + let result = ''; + function keep(node) { + result += text.substring(node.pos, node.end); + } + function write(data) { + result += data; + } + function writeMarkedNodes(node) { + if (getColor(node) === 2 /* Black */) { + return keep(node); + } + // Always keep certain top-level statements + if (ts.isSourceFile(node.parent)) { + if (ts.isExpressionStatement(node) && ts.isStringLiteral(node.expression) && node.expression.text === 'use strict') { + return keep(node); + } + if (ts.isVariableStatement(node) && nodeOrChildIsBlack(node)) { + return keep(node); + } + } + // Keep the entire import in import * as X cases + if (ts.isImportDeclaration(node)) { + if (node.importClause && node.importClause.namedBindings) { + if (ts.isNamespaceImport(node.importClause.namedBindings)) { + if (getColor(node.importClause.namedBindings) === 2 /* Black */) { + return keep(node); + } + } + else { + let survivingImports = []; + for (const importNode of node.importClause.namedBindings.elements) { + if (getColor(importNode) === 2 /* Black */) { + survivingImports.push(importNode.getFullText(sourceFile)); + } + } + const leadingTriviaWidth = node.getLeadingTriviaWidth(); + const leadingTrivia = sourceFile.text.substr(node.pos, leadingTriviaWidth); + if (survivingImports.length > 0) { + if (node.importClause && node.importClause.name && getColor(node.importClause) === 2 /* Black */) { + return write(`${leadingTrivia}import ${node.importClause.name.text}, {${survivingImports.join(',')} } from${node.moduleSpecifier.getFullText(sourceFile)};`); + } + return write(`${leadingTrivia}import {${survivingImports.join(',')} } from${node.moduleSpecifier.getFullText(sourceFile)};`); + } + else { + if (node.importClause && node.importClause.name && getColor(node.importClause) === 2 /* Black */) { + return write(`${leadingTrivia}import ${node.importClause.name.text} from${node.moduleSpecifier.getFullText(sourceFile)};`); + } + } + } + } + else { + if (node.importClause && getColor(node.importClause) === 2 /* Black */) { + return keep(node); + } + } + } + if (ts.isExportDeclaration(node)) { + if (node.exportClause && node.moduleSpecifier && ts.isNamedExports(node.exportClause)) { + let survivingExports = []; + for (const exportSpecifier of node.exportClause.elements) { + if (getColor(exportSpecifier) === 2 /* Black */) { + survivingExports.push(exportSpecifier.getFullText(sourceFile)); + } + } + const leadingTriviaWidth = node.getLeadingTriviaWidth(); + const leadingTrivia = sourceFile.text.substr(node.pos, leadingTriviaWidth); + if (survivingExports.length > 0) { + return write(`${leadingTrivia}export {${survivingExports.join(',')} } from${node.moduleSpecifier.getFullText(sourceFile)};`); + } + } + } + if (shakeLevel === 2 /* ClassMembers */ && (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) && nodeOrChildIsBlack(node)) { + let toWrite = node.getFullText(); + for (let i = node.members.length - 1; i >= 0; i--) { + const member = node.members[i]; + if (getColor(member) === 2 /* Black */ || !member.name) { + // keep method + continue; + } + let pos = member.pos - node.pos; + let end = member.end - node.pos; + toWrite = toWrite.substring(0, pos) + toWrite.substring(end); + } + return write(toWrite); + } + if (ts.isFunctionDeclaration(node)) { + // Do not go inside functions if they haven't been marked + return; + } + node.forEachChild(writeMarkedNodes); + } + if (getColor(sourceFile) !== 2 /* Black */) { + if (!nodeOrChildIsBlack(sourceFile)) { + // none of the elements are reachable => don't write this file at all! + return; + } + sourceFile.forEachChild(writeMarkedNodes); + result += sourceFile.endOfFileToken.getFullText(sourceFile); + } + else { + result = text; + } + writeFile(destination, result); + }); + return result; +} +//#endregion +//#region Utils +function isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(ts, program, checker, declaration) { + if (!program.isSourceFileDefaultLibrary(declaration.getSourceFile()) && declaration.heritageClauses) { + for (const heritageClause of declaration.heritageClauses) { + for (const type of heritageClause.types) { + const symbol = findSymbolFromHeritageType(ts, checker, type); + if (symbol) { + const decl = symbol.valueDeclaration || (symbol.declarations && symbol.declarations[0]); + if (decl && program.isSourceFileDefaultLibrary(decl.getSourceFile())) { + return true; + } + } + } + } + } + return false; +} +function findSymbolFromHeritageType(ts, checker, type) { + if (ts.isExpressionWithTypeArguments(type)) { + return findSymbolFromHeritageType(ts, checker, type.expression); + } + if (ts.isIdentifier(type)) { + return getRealNodeSymbol(ts, checker, type)[0]; + } + if (ts.isPropertyAccessExpression(type)) { + return findSymbolFromHeritageType(ts, checker, type.name); + } + return null; +} +/** + * Returns the node's symbol and the `import` node (if the symbol resolved from a different module) + */ +function getRealNodeSymbol(ts, checker, node) { + const getPropertySymbolsFromContextualType = ts.getPropertySymbolsFromContextualType; + const getContainingObjectLiteralElement = ts.getContainingObjectLiteralElement; + const getNameFromPropertyName = ts.getNameFromPropertyName; + // Go to the original declaration for cases: + // + // (1) when the aliased symbol was declared in the location(parent). + // (2) when the aliased symbol is originating from an import. + // + function shouldSkipAlias(node, declaration) { + if (!ts.isShorthandPropertyAssignment(node) && node.kind !== ts.SyntaxKind.Identifier) { + return false; + } + if (node.parent === declaration) { + return true; + } + switch (declaration.kind) { + case ts.SyntaxKind.ImportClause: + case ts.SyntaxKind.ImportEqualsDeclaration: + return true; + case ts.SyntaxKind.ImportSpecifier: + return declaration.parent.kind === ts.SyntaxKind.NamedImports; + default: + return false; + } + } + if (!ts.isShorthandPropertyAssignment(node)) { + if (node.getChildCount() !== 0) { + return [null, null]; + } + } + const { parent } = node; + let symbol = (ts.isShorthandPropertyAssignment(node) + ? checker.getShorthandAssignmentValueSymbol(node) + : checker.getSymbolAtLocation(node)); + let importNode = null; + // If this is an alias, and the request came at the declaration location + // get the aliased symbol instead. This allows for goto def on an import e.g. + // import {A, B} from "mod"; + // to jump to the implementation directly. + if (symbol && symbol.flags & ts.SymbolFlags.Alias && shouldSkipAlias(node, symbol.declarations[0])) { + const aliased = checker.getAliasedSymbol(symbol); + if (aliased.declarations) { + // We should mark the import as visited + importNode = symbol.declarations[0]; + symbol = aliased; + } + } + if (symbol) { + // Because name in short-hand property assignment has two different meanings: property name and property value, + // using go-to-definition at such position should go to the variable declaration of the property value rather than + // go to the declaration of the property name (in this case stay at the same position). However, if go-to-definition + // is performed at the location of property access, we would like to go to definition of the property in the short-hand + // assignment. This case and others are handled by the following code. + if (node.parent.kind === ts.SyntaxKind.ShorthandPropertyAssignment) { + symbol = checker.getShorthandAssignmentValueSymbol(symbol.valueDeclaration); + } + // If the node is the name of a BindingElement within an ObjectBindingPattern instead of just returning the + // declaration the symbol (which is itself), we should try to get to the original type of the ObjectBindingPattern + // and return the property declaration for the referenced property. + // For example: + // import('./foo').then(({ b/*goto*/ar }) => undefined); => should get use to the declaration in file "./foo" + // + // function bar(onfulfilled: (value: T) => void) { //....} + // interface Test { + // pr/*destination*/op1: number + // } + // bar(({pr/*goto*/op1})=>{}); + if (ts.isPropertyName(node) && ts.isBindingElement(parent) && ts.isObjectBindingPattern(parent.parent) && + (node === (parent.propertyName || parent.name))) { + const name = getNameFromPropertyName(node); + const type = checker.getTypeAtLocation(parent.parent); + if (name && type) { + if (type.isUnion()) { + const prop = type.types[0].getProperty(name); + if (prop) { + symbol = prop; + } + } + else { + const prop = type.getProperty(name); + if (prop) { + symbol = prop; + } + } + } + } + // If the current location we want to find its definition is in an object literal, try to get the contextual type for the + // object literal, lookup the property symbol in the contextual type, and use this for goto-definition. + // For example + // interface Props{ + // /*first*/prop1: number + // prop2: boolean + // } + // function Foo(arg: Props) {} + // Foo( { pr/*1*/op1: 10, prop2: false }) + const element = getContainingObjectLiteralElement(node); + if (element) { + const contextualType = element && checker.getContextualType(element.parent); + if (contextualType) { + const propertySymbols = getPropertySymbolsFromContextualType(element, checker, contextualType, /*unionSymbolOk*/ false); + if (propertySymbols) { + symbol = propertySymbols[0]; + } + } + } + } + if (symbol && symbol.declarations) { + return [symbol, importNode]; + } + return [null, null]; +} +/** Get the token whose text contains the position */ +function getTokenAtPosition(ts, sourceFile, position, allowPositionInLeadingTrivia, includeEndPosition) { + let current = sourceFile; + outer: while (true) { + // find the child that contains 'position' + for (const child of current.getChildren()) { + const start = allowPositionInLeadingTrivia ? child.getFullStart() : child.getStart(sourceFile, /*includeJsDoc*/ true); + if (start > position) { + // If this child begins after position, then all subsequent children will as well. + break; + } + const end = child.getEnd(); + if (position < end || (position === end && (child.kind === ts.SyntaxKind.EndOfFileToken || includeEndPosition))) { + current = child; + continue outer; + } + } + return current; + } +} diff --git a/build/lib/treeshaking.ts b/build/lib/treeshaking.ts index 405336bf..cb042679 100644 --- a/build/lib/treeshaking.ts +++ b/build/lib/treeshaking.ts @@ -7,7 +7,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import * as ts from 'typescript'; +import type * as ts from 'typescript'; const TYPESCRIPT_LIB_FOLDER = path.dirname(require.resolve('typescript/lib/lib.d.ts')); @@ -82,7 +82,8 @@ function printDiagnostics(options: ITreeShakingOptions, diagnostics: ReadonlyArr } export function shake(options: ITreeShakingOptions): ITreeShakingResult { - const languageService = createTypeScriptLanguageService(options); + const ts = require('typescript') as typeof import('typescript'); + const languageService = createTypeScriptLanguageService(ts, options); const program = languageService.getProgram()!; const globalDiagnostics = program.getGlobalDiagnostics(); @@ -103,15 +104,15 @@ export function shake(options: ITreeShakingOptions): ITreeShakingResult { throw new Error(`Compilation Errors encountered.`); } - markNodes(languageService, options); + markNodes(ts, languageService, options); - return generateResult(languageService, options.shakeLevel); + return generateResult(ts, languageService, options.shakeLevel); } //#region Discovery, LanguageService & Setup -function createTypeScriptLanguageService(options: ITreeShakingOptions): ts.LanguageService { +function createTypeScriptLanguageService(ts: typeof import('typescript'), options: ITreeShakingOptions): ts.LanguageService { // Discover referenced files - const FILES = discoverAndReadFiles(options); + const FILES = discoverAndReadFiles(ts, options); // Add fake usage files options.inlineEntryPoints.forEach((inlineEntryPoint, index) => { @@ -125,18 +126,18 @@ function createTypeScriptLanguageService(options: ITreeShakingOptions): ts.Langu }); // Resolve libs - const RESOLVED_LIBS = processLibFiles(options); + const RESOLVED_LIBS = processLibFiles(ts, options); const compilerOptions = ts.convertCompilerOptionsFromJson(options.compilerOptions, options.sourcesRoot).options; - const host = new TypeScriptLanguageServiceHost(RESOLVED_LIBS, FILES, compilerOptions); + const host = new TypeScriptLanguageServiceHost(ts, RESOLVED_LIBS, FILES, compilerOptions); return ts.createLanguageService(host); } /** * Read imports and follow them until all files have been handled */ -function discoverAndReadFiles(options: ITreeShakingOptions): IFileMap { +function discoverAndReadFiles(ts: typeof import('typescript'), options: ITreeShakingOptions): IFileMap { const FILES: IFileMap = {}; const in_queue: { [module: string]: boolean; } = Object.create(null); @@ -199,7 +200,7 @@ function discoverAndReadFiles(options: ITreeShakingOptions): IFileMap { /** * Read lib files and follow lib references */ -function processLibFiles(options: ITreeShakingOptions): ILibMap { +function processLibFiles(ts: typeof import('typescript'), options: ITreeShakingOptions): ILibMap { const stack: string[] = [...options.compilerOptions.lib]; const result: ILibMap = {}; @@ -232,11 +233,13 @@ interface IFileMap { [fileName: string]: string; } */ class TypeScriptLanguageServiceHost implements ts.LanguageServiceHost { + private readonly _ts: typeof import('typescript'); private readonly _libs: ILibMap; private readonly _files: IFileMap; private readonly _compilerOptions: ts.CompilerOptions; - constructor(libs: ILibMap, files: IFileMap, compilerOptions: ts.CompilerOptions) { + constructor(ts: typeof import('typescript'), libs: ILibMap, files: IFileMap, compilerOptions: ts.CompilerOptions) { + this._ts = ts; this._libs = libs; this._files = files; this._compilerOptions = compilerOptions; @@ -262,15 +265,15 @@ class TypeScriptLanguageServiceHost implements ts.LanguageServiceHost { } getScriptSnapshot(fileName: string): ts.IScriptSnapshot { if (this._files.hasOwnProperty(fileName)) { - return ts.ScriptSnapshot.fromString(this._files[fileName]); + return this._ts.ScriptSnapshot.fromString(this._files[fileName]); } else if (this._libs.hasOwnProperty(fileName)) { - return ts.ScriptSnapshot.fromString(this._libs[fileName]); + return this._ts.ScriptSnapshot.fromString(this._libs[fileName]); } else { - return ts.ScriptSnapshot.fromString(''); + return this._ts.ScriptSnapshot.fromString(''); } } getScriptKind(_fileName: string): ts.ScriptKind { - return ts.ScriptKind.TS; + return this._ts.ScriptKind.TS; } getCurrentDirectory(): string { return ''; @@ -320,7 +323,7 @@ function nodeOrChildIsBlack(node: ts.Node): boolean { return false; } -function markNodes(languageService: ts.LanguageService, options: ITreeShakingOptions) { +function markNodes(ts: typeof import('typescript'), languageService: ts.LanguageService, options: ITreeShakingOptions) { const program = languageService.getProgram(); if (!program) { throw new Error('Could not get program from language service'); @@ -446,7 +449,7 @@ function markNodes(languageService: ts.LanguageService, options: ITreeShakingOpt continue; } - const referenceNode = getTokenAtPosition(referenceSourceFile, reference.textSpan.start, false, false); + const referenceNode = getTokenAtPosition(ts, referenceSourceFile, reference.textSpan.start, false, false); if ( ts.isMethodDeclaration(referenceNode.parent) || ts.isPropertyDeclaration(referenceNode.parent) @@ -522,7 +525,7 @@ function markNodes(languageService: ts.LanguageService, options: ITreeShakingOpt const nodeSourceFile = node.getSourceFile(); const loop = (node: ts.Node) => { - const [symbol, symbolImportNode] = getRealNodeSymbol(checker, node); + const [symbol, symbolImportNode] = getRealNodeSymbol(ts, checker, node); if (symbolImportNode) { setColor(symbolImportNode, NodeColor.Black); } @@ -536,7 +539,7 @@ function markNodes(languageService: ts.LanguageService, options: ITreeShakingOpt continue; } - if (options.shakeLevel === ShakeLevel.ClassMembers && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && !isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(program, checker, declaration)) { + if (options.shakeLevel === ShakeLevel.ClassMembers && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && !isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(ts, program, checker, declaration)) { enqueue_black(declaration.name!); for (let j = 0; j < declaration.members.length; j++) { @@ -607,7 +610,7 @@ function nodeIsInItsOwnDeclaration(nodeSourceFile: ts.SourceFile, node: ts.Node, return false; } -function generateResult(languageService: ts.LanguageService, shakeLevel: ShakeLevel): ITreeShakingResult { +function generateResult(ts: typeof import('typescript'), languageService: ts.LanguageService, shakeLevel: ShakeLevel): ITreeShakingResult { const program = languageService.getProgram(); if (!program) { throw new Error('Could not get program from language service'); @@ -752,11 +755,11 @@ function generateResult(languageService: ts.LanguageService, shakeLevel: ShakeLe //#region Utils -function isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(program: ts.Program, checker: ts.TypeChecker, declaration: ts.ClassDeclaration | ts.InterfaceDeclaration): boolean { +function isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(ts: typeof import('typescript'), program: ts.Program, checker: ts.TypeChecker, declaration: ts.ClassDeclaration | ts.InterfaceDeclaration): boolean { if (!program.isSourceFileDefaultLibrary(declaration.getSourceFile()) && declaration.heritageClauses) { for (const heritageClause of declaration.heritageClauses) { for (const type of heritageClause.types) { - const symbol = findSymbolFromHeritageType(checker, type); + const symbol = findSymbolFromHeritageType(ts, checker, type); if (symbol) { const decl = symbol.valueDeclaration || (symbol.declarations && symbol.declarations[0]); if (decl && program.isSourceFileDefaultLibrary(decl.getSourceFile())) { @@ -769,15 +772,15 @@ function isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(program: ts.Progra return false; } -function findSymbolFromHeritageType(checker: ts.TypeChecker, type: ts.ExpressionWithTypeArguments | ts.Expression | ts.PrivateIdentifier): ts.Symbol | null { +function findSymbolFromHeritageType(ts: typeof import('typescript'), checker: ts.TypeChecker, type: ts.ExpressionWithTypeArguments | ts.Expression | ts.PrivateIdentifier): ts.Symbol | null { if (ts.isExpressionWithTypeArguments(type)) { - return findSymbolFromHeritageType(checker, type.expression); + return findSymbolFromHeritageType(ts, checker, type.expression); } if (ts.isIdentifier(type)) { - return getRealNodeSymbol(checker, type)[0]; + return getRealNodeSymbol(ts, checker, type)[0]; } if (ts.isPropertyAccessExpression(type)) { - return findSymbolFromHeritageType(checker, type.name); + return findSymbolFromHeritageType(ts, checker, type.name); } return null; } @@ -785,7 +788,7 @@ function findSymbolFromHeritageType(checker: ts.TypeChecker, type: ts.Expression /** * Returns the node's symbol and the `import` node (if the symbol resolved from a different module) */ -function getRealNodeSymbol(checker: ts.TypeChecker, node: ts.Node): [ts.Symbol | null, ts.Declaration | null] { +function getRealNodeSymbol(ts: typeof import('typescript'), checker: ts.TypeChecker, node: ts.Node): [ts.Symbol | null, ts.Declaration | null] { // Use some TypeScript internals to avoid code duplication type ObjectLiteralElementWithName = ts.ObjectLiteralElement & { name: ts.PropertyName; parent: ts.ObjectLiteralExpression | ts.JsxAttributes }; @@ -913,7 +916,7 @@ function getRealNodeSymbol(checker: ts.TypeChecker, node: ts.Node): [ts.Symbol | } /** Get the token whose text contains the position */ -function getTokenAtPosition(sourceFile: ts.SourceFile, position: number, allowPositionInLeadingTrivia: boolean, includeEndPosition: boolean): ts.Node { +function getTokenAtPosition(ts: typeof import('typescript'), sourceFile: ts.SourceFile, position: number, allowPositionInLeadingTrivia: boolean, includeEndPosition: boolean): ts.Node { let current: ts.Node = sourceFile; outer: while (true) { // find the child that contains 'position' diff --git a/build/lib/typings/gulp-cssnano.d.ts b/build/lib/typings/gulp-cssnano.d.ts deleted file mode 100644 index 97d38276..00000000 --- a/build/lib/typings/gulp-cssnano.d.ts +++ /dev/null @@ -1,12 +0,0 @@ - -declare module "gulp-cssnano" { - function f(opts:{reduceIdents:boolean;}): NodeJS.ReadWriteStream; - - /** - * This is required as per: - * https://github.com/microsoft/TypeScript/issues/5073 - */ - namespace f {} - - export = f; -} diff --git a/build/lib/util.js b/build/lib/util.js new file mode 100644 index 00000000..8d0294e4 --- /dev/null +++ b/build/lib/util.js @@ -0,0 +1,276 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getElectronVersion = exports.streamToPromise = exports.versionStringToNumber = exports.filter = exports.rebase = exports.getVersion = exports.ensureDir = exports.rreddir = exports.rimraf = exports.rewriteSourceMappingURL = exports.stripSourceMappingURL = exports.loadSourcemaps = exports.cleanNodeModules = exports.skipDirectories = exports.toFileUri = exports.setExecutableBit = exports.fixWin32DirectoryPermissions = exports.incremental = void 0; +const es = require("event-stream"); +const debounce = require("debounce"); +const _filter = require("gulp-filter"); +const rename = require("gulp-rename"); +const path = require("path"); +const fs = require("fs"); +const _rimraf = require("rimraf"); +const git = require("./git"); +const VinylFile = require("vinyl"); +const root = path.dirname(path.dirname(__dirname)); +const NoCancellationToken = { isCancellationRequested: () => false }; +function incremental(streamProvider, initial, supportsCancellation) { + const input = es.through(); + const output = es.through(); + let state = 'idle'; + let buffer = Object.create(null); + const token = !supportsCancellation ? undefined : { isCancellationRequested: () => Object.keys(buffer).length > 0 }; + const run = (input, isCancellable) => { + state = 'running'; + const stream = !supportsCancellation ? streamProvider() : streamProvider(isCancellable ? token : NoCancellationToken); + input + .pipe(stream) + .pipe(es.through(undefined, () => { + state = 'idle'; + eventuallyRun(); + })) + .pipe(output); + }; + if (initial) { + run(initial, false); + } + const eventuallyRun = debounce(() => { + const paths = Object.keys(buffer); + if (paths.length === 0) { + return; + } + const data = paths.map(path => buffer[path]); + buffer = Object.create(null); + run(es.readArray(data), true); + }, 500); + input.on('data', (f) => { + buffer[f.path] = f; + if (state === 'idle') { + eventuallyRun(); + } + }); + return es.duplex(input, output); +} +exports.incremental = incremental; +function fixWin32DirectoryPermissions() { + if (!/win32/.test(process.platform)) { + return es.through(); + } + return es.mapSync(f => { + if (f.stat && f.stat.isDirectory && f.stat.isDirectory()) { + f.stat.mode = 16877; + } + return f; + }); +} +exports.fixWin32DirectoryPermissions = fixWin32DirectoryPermissions; +function setExecutableBit(pattern) { + const setBit = es.mapSync(f => { + if (!f.stat) { + f.stat = { isFile() { return true; } }; + } + f.stat.mode = /* 100755 */ 33261; + return f; + }); + if (!pattern) { + return setBit; + } + const input = es.through(); + const filter = _filter(pattern, { restore: true }); + const output = input + .pipe(filter) + .pipe(setBit) + .pipe(filter.restore); + return es.duplex(input, output); +} +exports.setExecutableBit = setExecutableBit; +function toFileUri(filePath) { + const match = filePath.match(/^([a-z])\:(.*)$/i); + if (match) { + filePath = '/' + match[1].toUpperCase() + ':' + match[2]; + } + return 'file://' + filePath.replace(/\\/g, '/'); +} +exports.toFileUri = toFileUri; +function skipDirectories() { + return es.mapSync(f => { + if (!f.isDirectory()) { + return f; + } + }); +} +exports.skipDirectories = skipDirectories; +function cleanNodeModules(rulePath) { + const rules = fs.readFileSync(rulePath, 'utf8') + .split(/\r?\n/g) + .map(line => line.trim()) + .filter(line => line && !/^#/.test(line)); + const excludes = rules.filter(line => !/^!/.test(line)).map(line => `!**/node_modules/${line}`); + const includes = rules.filter(line => /^!/.test(line)).map(line => `**/node_modules/${line.substr(1)}`); + const input = es.through(); + const output = es.merge(input.pipe(_filter(['**', ...excludes])), input.pipe(_filter(includes))); + return es.duplex(input, output); +} +exports.cleanNodeModules = cleanNodeModules; +function loadSourcemaps() { + const input = es.through(); + const output = input + .pipe(es.map((f, cb) => { + if (f.sourceMap) { + cb(undefined, f); + return; + } + if (!f.contents) { + cb(undefined, f); + return; + } + const contents = f.contents.toString('utf8'); + const reg = /\/\/# sourceMappingURL=(.*)$/g; + let lastMatch = null; + let match = null; + while (match = reg.exec(contents)) { + lastMatch = match; + } + if (!lastMatch) { + f.sourceMap = { + version: '3', + names: [], + mappings: '', + sources: [f.relative], + sourcesContent: [contents] + }; + cb(undefined, f); + return; + } + f.contents = Buffer.from(contents.replace(/\/\/# sourceMappingURL=(.*)$/g, ''), 'utf8'); + fs.readFile(path.join(path.dirname(f.path), lastMatch[1]), 'utf8', (err, contents) => { + if (err) { + return cb(err); + } + f.sourceMap = JSON.parse(contents); + cb(undefined, f); + }); + })); + return es.duplex(input, output); +} +exports.loadSourcemaps = loadSourcemaps; +function stripSourceMappingURL() { + const input = es.through(); + const output = input + .pipe(es.mapSync(f => { + const contents = f.contents.toString('utf8'); + f.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, ''), 'utf8'); + return f; + })); + return es.duplex(input, output); +} +exports.stripSourceMappingURL = stripSourceMappingURL; +function rewriteSourceMappingURL(sourceMappingURLBase) { + const input = es.through(); + const output = input + .pipe(es.mapSync(f => { + const contents = f.contents.toString('utf8'); + const str = `//# sourceMappingURL=${sourceMappingURLBase}/${path.dirname(f.relative).replace(/\\/g, '/')}/$1`; + f.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, str)); + return f; + })); + return es.duplex(input, output); +} +exports.rewriteSourceMappingURL = rewriteSourceMappingURL; +function rimraf(dir) { + const result = () => new Promise((c, e) => { + let retries = 0; + const retry = () => { + _rimraf(dir, { maxBusyTries: 1 }, (err) => { + if (!err) { + return c(); + } + if (err.code === 'ENOTEMPTY' && ++retries < 5) { + return setTimeout(() => retry(), 10); + } + return e(err); + }); + }; + retry(); + }); + result.taskName = `clean-${path.basename(dir).toLowerCase()}`; + return result; +} +exports.rimraf = rimraf; +function _rreaddir(dirPath, prepend, result) { + const entries = fs.readdirSync(dirPath, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory()) { + _rreaddir(path.join(dirPath, entry.name), `${prepend}/${entry.name}`, result); + } + else { + result.push(`${prepend}/${entry.name}`); + } + } +} +function rreddir(dirPath) { + let result = []; + _rreaddir(dirPath, '', result); + return result; +} +exports.rreddir = rreddir; +function ensureDir(dirPath) { + if (fs.existsSync(dirPath)) { + return; + } + ensureDir(path.dirname(dirPath)); + fs.mkdirSync(dirPath); +} +exports.ensureDir = ensureDir; +function getVersion(root) { + let version = process.env['BUILD_SOURCEVERSION']; + if (!version || !/^[0-9a-f]{40}$/i.test(version)) { + version = git.getVersion(root); + } + return version; +} +exports.getVersion = getVersion; +function rebase(count) { + return rename(f => { + const parts = f.dirname ? f.dirname.split(/[\/\\]/) : []; + f.dirname = parts.slice(count).join(path.sep); + }); +} +exports.rebase = rebase; +function filter(fn) { + const result = es.through(function (data) { + if (fn(data)) { + this.emit('data', data); + } + else { + result.restore.push(data); + } + }); + result.restore = es.through(); + return result; +} +exports.filter = filter; +function versionStringToNumber(versionStr) { + const semverRegex = /(\d+)\.(\d+)\.(\d+)/; + const match = versionStr.match(semverRegex); + if (!match) { + throw new Error('Version string is not properly formatted: ' + versionStr); + } + return parseInt(match[1], 10) * 1e4 + parseInt(match[2], 10) * 1e2 + parseInt(match[3], 10); +} +exports.versionStringToNumber = versionStringToNumber; +function streamToPromise(stream) { + return new Promise((c, e) => { + stream.on('error', err => e(err)); + stream.on('end', () => c()); + }); +} +exports.streamToPromise = streamToPromise; +function getElectronVersion() { + const yarnrc = fs.readFileSync(path.join(root, '.yarnrc'), 'utf8'); + const target = /^target "(.*)"$/m.exec(yarnrc)[1]; + return target; +} +exports.getElectronVersion = getElectronVersion; diff --git a/src/vs/editor/contrib/documentSymbols/media/symbol-icons.css b/build/lib/watch/index.js similarity index 64% rename from src/vs/editor/contrib/documentSymbols/media/symbol-icons.css rename to build/lib/watch/index.js index a7ba268d..949cb91c 100644 --- a/src/vs/editor/contrib/documentSymbols/media/symbol-icons.css +++ b/build/lib/watch/index.js @@ -1,9 +1,9 @@ +"use strict"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - -.monaco-icon-label.deprecated { - text-decoration: line-through; - opacity: 0.66; -} +const watch = process.platform === 'win32' ? require('./watch-win32') : require('vscode-gulp-watch'); +module.exports = function () { + return watch.apply(null, arguments); +}; diff --git a/build/lib/watch/package.json b/build/lib/watch/package.json index d509f873..e2e4f552 100644 --- a/build/lib/watch/package.json +++ b/build/lib/watch/package.json @@ -7,6 +7,6 @@ "license": "MIT", "devDependencies": {}, "dependencies": { - "vscode-gulp-watch": "^5.0.2" + "vscode-gulp-watch": "^5.0.3" } } diff --git a/build/lib/watch/watch-win32.js b/build/lib/watch/watch-win32.js new file mode 100644 index 00000000..71681c9c --- /dev/null +++ b/build/lib/watch/watch-win32.js @@ -0,0 +1,100 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +const path = require("path"); +const cp = require("child_process"); +const fs = require("fs"); +const File = require("vinyl"); +const es = require("event-stream"); +const filter = require("gulp-filter"); +const watcherPath = path.join(__dirname, 'watcher.exe'); +function toChangeType(type) { + switch (type) { + case '0': return 'change'; + case '1': return 'add'; + default: return 'unlink'; + } +} +function watch(root) { + const result = es.through(); + let child = cp.spawn(watcherPath, [root]); + child.stdout.on('data', function (data) { + const lines = data.toString('utf8').split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (line.length === 0) { + continue; + } + const changeType = line[0]; + const changePath = line.substr(2); + // filter as early as possible + if (/^\.git/.test(changePath) || /(^|\\)out($|\\)/.test(changePath)) { + continue; + } + const changePathFull = path.join(root, changePath); + const file = new File({ + path: changePathFull, + base: root + }); + file.event = toChangeType(changeType); + result.emit('data', file); + } + }); + child.stderr.on('data', function (data) { + result.emit('error', data); + }); + child.on('exit', function (code) { + result.emit('error', 'Watcher died with code ' + code); + child = null; + }); + process.once('SIGTERM', function () { process.exit(0); }); + process.once('SIGTERM', function () { process.exit(0); }); + process.once('exit', function () { if (child) { + child.kill(); + } }); + return result; +} +const cache = Object.create(null); +module.exports = function (pattern, options) { + options = options || {}; + const cwd = path.normalize(options.cwd || process.cwd()); + let watcher = cache[cwd]; + if (!watcher) { + watcher = cache[cwd] = watch(cwd); + } + const rebase = !options.base ? es.through() : es.mapSync(function (f) { + f.base = options.base; + return f; + }); + return watcher + .pipe(filter(['**', '!.git{,/**}'])) // ignore all things git + .pipe(filter(pattern)) + .pipe(es.map(function (file, cb) { + fs.stat(file.path, function (err, stat) { + if (err && err.code === 'ENOENT') { + return cb(undefined, file); + } + if (err) { + return cb(); + } + if (!stat.isFile()) { + return cb(); + } + fs.readFile(file.path, function (err, contents) { + if (err && err.code === 'ENOENT') { + return cb(undefined, file); + } + if (err) { + return cb(); + } + file.contents = contents; + file.stat = stat; + cb(undefined, file); + }); + }); + })) + .pipe(rebase); +}; diff --git a/build/lib/watch/watch-win32.ts b/build/lib/watch/watch-win32.ts index 4ed37277..833c8d9c 100644 --- a/build/lib/watch/watch-win32.ts +++ b/build/lib/watch/watch-win32.ts @@ -25,7 +25,7 @@ function watch(root: string): Stream { const result = es.through(); let child: cp.ChildProcess | null = cp.spawn(watcherPath, [root]); - child.stdout.on('data', function (data) { + child.stdout!.on('data', function (data) { const lines: string[] = data.toString('utf8').split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); @@ -52,7 +52,7 @@ function watch(root: string): Stream { } }); - child.stderr.on('data', function (data) { + child.stderr!.on('data', function (data) { result.emit('error', data); }); diff --git a/build/lib/watch/yarn.lock b/build/lib/watch/yarn.lock index edbfe1f3..b0d7dd4a 100644 --- a/build/lib/watch/yarn.lock +++ b/build/lib/watch/yarn.lock @@ -2,7 +2,12 @@ # yarn lockfile v1 -ansi-colors@1.1.0, ansi-colors@^1.0.1: +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-colors@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-1.1.0.tgz#6374b4dd5d4718ff3ce27a671a3b1cad077132a9" integrity sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA== @@ -21,15 +26,7 @@ ansi-wrap@0.1.0, ansi-wrap@^0.1.0: resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= -anymatch@^1.3.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" - integrity sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA== - dependencies: - micromatch "^2.1.5" - normalize-path "^2.0.0" - -anymatch@~3.1.1: +anymatch@^3.1.1, anymatch@~3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== @@ -37,33 +34,16 @@ anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" -arr-diff@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" - integrity sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8= - dependencies: - arr-flatten "^1.0.1" - arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= -arr-flatten@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - arr-union@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= -array-unique@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" - integrity sha1-odl8yvy8JiXMcPrc6zalDFiwGlM= - assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" @@ -74,15 +54,6 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== -braces@^1.8.2: - version "1.8.5" - resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" - integrity sha1-uneWLhLf+WnWt2cR6RS3N4V79qc= - dependencies: - expand-range "^1.8.1" - preserve "^0.2.0" - repeat-element "^1.1.2" - braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -90,10 +61,10 @@ braces@~3.0.2: dependencies: fill-range "^7.0.1" -chokidar@3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.0.tgz#12c0714668c55800f659e262d4962a97faf554a6" - integrity sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A== +chokidar@3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" + integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== dependencies: anymatch "~3.1.1" braces "~3.0.2" @@ -101,30 +72,20 @@ chokidar@3.3.0: is-binary-path "~2.1.0" is-glob "~4.0.1" normalize-path "~3.0.0" - readdirp "~3.2.0" + readdirp "~3.5.0" optionalDependencies: - fsevents "~2.1.1" + fsevents "~2.3.1" clone-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= -clone-stats@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1" - integrity sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE= - clone-stats@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" integrity sha1-s3gt/4u1R04Yuba/D9/ngvh3doA= -clone@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" - integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= - clone@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" @@ -149,20 +110,6 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= -expand-brackets@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" - integrity sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s= - dependencies: - is-posix-bracket "^0.1.0" - -expand-range@^1.8.1: - version "1.8.2" - resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" - integrity sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc= - dependencies: - fill-range "^2.1.0" - extend-shallow@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" @@ -171,38 +118,16 @@ extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extglob@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" - integrity sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE= - dependencies: - is-extglob "^1.0.0" - -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= +fancy-log@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.3.tgz#dbc19154f558690150a23953a0adbd035be45fc7" + integrity sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw== dependencies: ansi-gray "^0.1.1" color-support "^1.1.3" + parse-node-version "^1.0.0" time-stamp "^1.0.0" -filename-regex@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" - integrity sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY= - -fill-range@^2.1.0: - version "2.2.4" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565" - integrity sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q== - dependencies: - is-number "^2.1.0" - isobject "^2.0.0" - randomatic "^3.0.0" - repeat-element "^1.1.2" - repeat-string "^1.5.2" - fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -217,47 +142,12 @@ first-chunk-stream@^2.0.0: dependencies: readable-stream "^2.0.2" -for-in@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= +fsevents@~2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.1.tgz#b209ab14c61012636c8863507edf7fb68cc54e9f" + integrity sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw== -for-own@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" - integrity sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4= - dependencies: - for-in "^1.0.1" - -fsevents@~2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805" - integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA== - -glob-base@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" - integrity sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q= - dependencies: - glob-parent "^2.0.0" - is-glob "^2.0.0" - -glob-parent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" - integrity sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg= - dependencies: - is-glob "^2.0.0" - -glob-parent@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= - dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" - -glob-parent@~5.1.0: +glob-parent@^5.1.1, glob-parent@~5.1.0: version "5.1.1" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== @@ -269,7 +159,7 @@ graceful-fs@^4.1.2: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== -inherits@^2.0.1, inherits@~2.0.3: +inherits@^2.0.1, 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== @@ -281,28 +171,6 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-dotfile@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" - integrity sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE= - -is-equal-shallow@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" - integrity sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ= - dependencies: - is-primitive "^2.0.0" - -is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= - is-extendable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" @@ -310,30 +178,11 @@ is-extendable@^1.0.1: dependencies: is-plain-object "^2.0.4" -is-extglob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" - integrity sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA= - -is-extglob@^2.1.0, is-extglob@^2.1.1: +is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= -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" - integrity sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM= - dependencies: - is-extglob "^1.0.0" - -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= - dependencies: - is-extglob "^2.1.0" - is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" @@ -341,18 +190,6 @@ is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" -is-number@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" - integrity sha1-Afy7s5NGOlSPL0ZszhbezknbkI8= - dependencies: - kind-of "^3.0.2" - -is-number@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" - integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== - is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -365,120 +202,37 @@ is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-posix-bracket@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" - integrity sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q= - -is-primitive@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" - integrity sha1-IHurkWOEmcB7Kt8kCkGochADRXU= - -is-utf8@^0.2.0: +is-utf8@^0.2.0, is-utf8@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= -isarray@1.0.0, isarray@~1.0.0: +isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= - dependencies: - isarray "1.0.0" - isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= -kind-of@^3.0.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" - -kind-of@^6.0.0: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -math-random@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c" - integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A== - -micromatch@^2.1.5: - version "2.3.11" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" - integrity sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU= - dependencies: - arr-diff "^2.0.0" - array-unique "^0.2.1" - braces "^1.8.2" - expand-brackets "^0.1.4" - extglob "^0.3.1" - filename-regex "^2.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.1" - kind-of "^3.0.2" - normalize-path "^2.0.1" - object.omit "^2.0.0" - parse-glob "^3.0.4" - regex-cache "^0.4.2" - -normalize-path@^2.0.0, normalize-path@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= - dependencies: - remove-trailing-separator "^1.0.1" - normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -object-assign@^4.1.0: +object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= -object.omit@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" - integrity sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo= - dependencies: - for-own "^0.1.4" - is-extendable "^0.1.1" - -parse-glob@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" - integrity sha1-ssN2z7EfNVE7rdFz7wu246OIORw= - dependencies: - glob-base "^0.3.0" - is-dotfile "^1.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.0" - -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= - -path-is-absolute@^1.0.1: +parse-node-version@^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= + resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" + integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== -picomatch@^2.0.4: +picomatch@^2.0.4, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== @@ -488,18 +242,6 @@ pify@^2.3.0: resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= - plugin-error@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c" @@ -510,26 +252,12 @@ plugin-error@1.0.1: arr-union "^3.1.0" extend-shallow "^3.0.2" -preserve@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" - integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks= - process-nextick-args@^2.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== -randomatic@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed" - integrity sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw== - dependencies: - is-number "^4.0.0" - kind-of "^6.0.0" - math-random "^1.0.1" - -readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@^2.3.5: +readable-stream@^2.0.2, readable-stream@^2.3.5: 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== @@ -542,40 +270,27 @@ readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@^2.3.5: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readdirp@~3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.2.0.tgz#c30c33352b12c96dfb4b895421a49fd5a9593839" - integrity sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ== +readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== dependencies: - picomatch "^2.0.4" + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" -regex-cache@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" - integrity sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ== +readdirp@~3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" + integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== dependencies: - is-equal-shallow "^0.1.3" + picomatch "^2.2.1" remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= -repeat-element@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" - integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== - -repeat-string@^1.5.2: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -replace-ext@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924" - integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ= - replace-ext@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" @@ -586,6 +301,18 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +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" @@ -593,6 +320,13 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +strip-bom-buf@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-bom-buf/-/strip-bom-buf-1.0.0.tgz#1cb45aaf57530f4caf86c7f75179d2c9a51dd572" + integrity sha1-HLRar1dTD0yvhsf3UXnSyaUd1XI= + dependencies: + is-utf8 "^0.2.1" + strip-bom-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz#f87db5ef2613f6968aa545abfe1ec728b6a829ca" @@ -620,36 +354,26 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -util-deprecate@~1.0.1: +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= -vinyl-file@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/vinyl-file/-/vinyl-file-2.0.0.tgz#a7ebf5ffbefda1b7d18d140fcb07b223efb6751a" - integrity sha1-p+v1/779obfRjRQPyweyI++2dRo= +vinyl-file@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/vinyl-file/-/vinyl-file-3.0.0.tgz#b104d9e4409ffa325faadd520642d0a3b488b365" + integrity sha1-sQTZ5ECf+jJfqt1SBkLQo7SIs2U= dependencies: graceful-fs "^4.1.2" pify "^2.3.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" + strip-bom-buf "^1.0.0" strip-bom-stream "^2.0.0" - vinyl "^1.1.0" + vinyl "^2.0.1" -vinyl@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884" - integrity sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ= - dependencies: - clone "^1.0.0" - clone-stats "^0.0.1" - replace-ext "0.0.1" - -vinyl@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.0.tgz#d85b07da96e458d25b2ffe19fece9f2caa13ed86" - integrity sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg== +vinyl@^2.0.1, vinyl@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.1.tgz#23cfb8bbab5ece3803aa2c0a1eb28af7cbba1974" + integrity sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw== dependencies: clone "^2.1.1" clone-buffer "^1.0.0" @@ -658,20 +382,19 @@ vinyl@^2.1.0: remove-trailing-separator "^1.0.1" replace-ext "^1.0.0" -vscode-gulp-watch@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/vscode-gulp-watch/-/vscode-gulp-watch-5.0.2.tgz#0060ba8d091284a6fbd7e608aa318a9c1d73b840" - integrity sha512-l2v+W3iQvxpX2ny2C7eJTd+83rQXiZ85KGY0mub/QRqUxgDc+KH/EYiw4mttzIhPzVBmxrUO4RcLNbPdccg0mQ== +vscode-gulp-watch@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/vscode-gulp-watch/-/vscode-gulp-watch-5.0.3.tgz#1ca1c03581d43692ecb1fe0b9afd4256faeb701b" + integrity sha512-MTUp2yLE9CshhkNSNV58EQNxQSeF8lIj3mkXZX9a1vAk+EQNM2PAYdPUDSd/P/08W3PMHGznEiZyfK7JAjLosg== dependencies: - ansi-colors "1.1.0" - anymatch "^1.3.0" - chokidar "3.3.0" - fancy-log "1.3.2" - glob-parent "^3.0.1" + ansi-colors "4.1.1" + anymatch "^3.1.1" + chokidar "3.5.1" + fancy-log "^1.3.3" + glob-parent "^5.1.1" normalize-path "^3.0.0" - object-assign "^4.1.0" - path-is-absolute "^1.0.1" + object-assign "^4.1.1" plugin-error "1.0.1" - readable-stream "^2.2.2" - vinyl "^2.1.0" - vinyl-file "^2.0.0" + readable-stream "^3.6.0" + vinyl "^2.2.0" + vinyl-file "^3.0.0" diff --git a/build/monaco/monaco.d.ts.recipe b/build/monaco/monaco.d.ts.recipe index 7288af94..0449ec6b 100644 --- a/build/monaco/monaco.d.ts.recipe +++ b/build/monaco/monaco.d.ts.recipe @@ -10,6 +10,7 @@ declare namespace monaco { export type Thenable = PromiseLike; export interface Environment { + globalAPI?: boolean; baseUrl?: string; getWorker?(workerId: string, label: string): Worker; getWorkerUrl?(workerId: string, label: string): string; diff --git a/build/monaco/package.json b/build/monaco/package.json index b42c6aa7..e26efaa5 100644 --- a/build/monaco/package.json +++ b/build/monaco/package.json @@ -1,7 +1,7 @@ { "name": "monaco-editor-core", "private": true, - "version": "0.21.1", + "version": "0.22.0", "description": "A browser based code editor", "author": "Microsoft Corporation", "license": "MIT", diff --git a/build/npm/dirs.js b/build/npm/dirs.js new file mode 100644 index 00000000..17e97a0c --- /dev/null +++ b/build/npm/dirs.js @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Complete list of directories where yarn should be executed to install node modules +exports.dirs = [ + '', + 'build', + 'build/lib/watch', + 'extensions', + 'extensions/configuration-editing', + 'extensions/css-language-features', + 'extensions/css-language-features/server', + 'extensions/debug-auto-launch', + 'extensions/debug-server-ready', + 'extensions/emmet', + 'extensions/extension-editing', + 'extensions/git', + 'extensions/git-ui', + 'extensions/github', + 'extensions/github-authentication', + 'extensions/grunt', + 'extensions/gulp', + 'extensions/html-language-features', + 'extensions/html-language-features/server', + 'extensions/image-preview', + 'extensions/jake', + 'extensions/json-language-features', + 'extensions/json-language-features/server', + 'extensions/markdown-language-features', + 'extensions/merge-conflict', + 'extensions/microsoft-authentication', + 'extensions/npm', + 'extensions/php-language-features', + 'extensions/search-result', + 'extensions/simple-browser', + 'extensions/testing-editor-contributions', + 'extensions/typescript-language-features', + 'extensions/vscode-api-tests', + 'extensions/vscode-colorize-tests', + 'extensions/vscode-custom-editor-tests', + 'extensions/vscode-notebook-tests', + 'extensions/vscode-test-resolver', + 'remote', + 'remote/web', + 'test/automation', + 'test/integration/browser', + 'test/monaco', + 'test/smoke', +]; diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index 8f8b0019..18ccef88 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -6,6 +6,7 @@ const cp = require('child_process'); const path = require('path'); const fs = require('fs'); +const { dirs } = require('./dirs'); const yarn = process.platform === 'win32' ? 'yarn.cmd' : 'yarn'; /** @@ -21,6 +22,10 @@ function yarnInstall(location, opts) { const argv = JSON.parse(raw); const original = argv.original || []; const args = original.filter(arg => arg === '--ignore-optional' || arg === '--frozen-lockfile'); + if (opts.ignoreEngines) { + args.push('--ignore-engines'); + delete opts.ignoreEngines; + } console.log(`Installing dependencies in ${location}...`); console.log(`$ yarn ${args.join(' ')}`); @@ -31,24 +36,39 @@ function yarnInstall(location, opts) { } } -yarnInstall('extensions'); // node modules shared by all extensions +for (let dir of dirs) { -if (!(process.platform === 'win32' && (process.arch === 'arm64' || process.env['npm_config_arch'] === 'arm64'))) { - yarnInstall('remote'); // node modules used by vscode server - yarnInstall('remote/web'); // node modules used by vscode web -} - -const allExtensionFolders = fs.readdirSync('extensions'); -const extensions = allExtensionFolders.filter(e => { - try { - let packageJSON = JSON.parse(fs.readFileSync(path.join('extensions', e, 'package.json')).toString()); - return packageJSON && (packageJSON.dependencies || packageJSON.devDependencies); - } catch (e) { - return false; + if (dir === '') { + // `yarn` already executed in root + continue; } -}); -extensions.forEach(extension => yarnInstall(`extensions/${extension}`)); + if (/^remote/.test(dir) && process.platform === 'win32' && (process.arch === 'arm64' || process.env['npm_config_arch'] === 'arm64')) { + // windows arm: do not execute `yarn` on remote folder + continue; + } + + if (dir === 'build/lib/watch') { + // node modules for watching, specific to host node version, not electron + yarnInstallBuildDependencies(); + continue; + } + + let opts; + + if (dir === 'remote') { + // node modules used by vscode server + const env = { ...process.env }; + if (process.env['VSCODE_REMOTE_CC']) { env['CC'] = process.env['VSCODE_REMOTE_CC']; } + if (process.env['VSCODE_REMOTE_CXX']) { env['CXX'] = process.env['VSCODE_REMOTE_CXX']; } + if (process.env['VSCODE_REMOTE_NODE_GYP']) { env['npm_config_node_gyp'] = process.env['VSCODE_REMOTE_NODE_GYP']; } + opts = { env }; + } else if (/^extensions\//.test(dir)) { + opts = { ignoreEngines: true }; + } + + yarnInstall(dir, opts); +} function yarnInstallBuildDependencies() { // make sure we install the deps of build/lib/watch for the system installed @@ -68,10 +88,4 @@ runtime "${runtime}"`; yarnInstall(watchPath); } -yarnInstall(`build`); // node modules required for build -yarnInstall('test/automation'); // node modules required for smoketest -yarnInstall('test/smoke'); // node modules required for smoketest -yarnInstall('test/integration/browser'); // node modules required for integration -yarnInstallBuildDependencies(); // node modules for watching, specific to host node version, not electron - cp.execSync('git config pull.rebase true'); diff --git a/build/npm/preinstall.js b/build/npm/preinstall.js index 94dbb3b9..8d51e7a7 100644 --- a/build/npm/preinstall.js +++ b/build/npm/preinstall.js @@ -28,7 +28,41 @@ if (!/yarn[\w-.]*\.js$|yarnpkg$/.test(process.env['npm_execpath'])) { err = true; } +if (process.platform === 'win32') { + if (!hasSupportedVisualStudioVersion()) { + console.error('\033[1;31m*** Invalid C/C++ Compiler Toolchain. Please check https://github.com/microsoft/vscode/wiki/How-to-Contribute.\033[0;0m'); + err = true; + } +} + if (err) { console.error(''); process.exit(1); } + +function hasSupportedVisualStudioVersion() { + const fs = require('fs'); + const path = require('path'); + // Translated over from + // https://source.chromium.org/chromium/chromium/src/+/master:build/vs_toolchain.py;l=140-175 + const supportedVersions = ['2019', '2017']; + + const availableVersions = []; + for (const version of supportedVersions) { + let vsPath = process.env[`vs${version}_install`]; + if (vsPath && fs.existsSync(vsPath)) { + availableVersions.push(version); + break; + } + const programFiles86Path = process.env['ProgramFiles(x86)']; + if (programFiles86Path) { + vsPath = `${programFiles86Path}/Microsoft Visual Studio/${version}`; + const vsTypes = ['Enterprise', 'Professional', 'Community', 'Preview', 'BuildTools']; + if (vsTypes.some(vsType => fs.existsSync(path.join(vsPath, vsType)))) { + availableVersions.push(version); + break; + } + } + } + return availableVersions.length; +} diff --git a/build/npm/update-theme.js b/build/npm/update-theme.js deleted file mode 100644 index 7e5649a1..00000000 --- a/build/npm/update-theme.js +++ /dev/null @@ -1,82 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -var path = require('path'); -var fs = require('fs'); -var plist = require('fast-plist'); - -var mappings = { - "background": ["editor.background"], - "foreground": ["editor.foreground"], - "hoverHighlight": ["editor.hoverHighlightBackground"], - "linkForeground": ["editorLink.foreground"], - "selection": ["editor.selectionBackground"], - "inactiveSelection": ["editor.inactiveSelectionBackground"], - "selectionHighlightColor": ["editor.selectionHighlightBackground"], - "wordHighlight": ["editor.wordHighlightBackground"], - "wordHighlightStrong": ["editor.wordHighlightStrongBackground"], - "findMatchHighlight": ["editor.findMatchHighlightBackground", "peekViewResult.matchHighlightBackground"], - "currentFindMatchHighlight": ["editor.findMatchBackground"], - "findRangeHighlight": ["editor.findRangeHighlightBackground"], - "referenceHighlight": ["peekViewEditor.matchHighlightBackground"], - "lineHighlight": ["editor.lineHighlightBackground"], - "rangeHighlight": ["editor.rangeHighlightBackground"], - "caret": ["editorCursor.foreground"], - "invisibles": ["editorWhitespace.foreground"], - "guide": ["editorIndentGuide.background"], - "ansiBlack": ["terminal.ansiBlack"], "ansiRed": ["terminal.ansiRed"], "ansiGreen": ["terminal.ansiGreen"], "ansiYellow": ["terminal.ansiYellow"], - "ansiBlue": ["terminal.ansiBlue"], "ansiMagenta": ["terminal.ansiMagenta"], "ansiCyan": ["terminal.ansiCyan"], "ansiWhite": ["terminal.ansiWhite"], - "ansiBrightBlack": ["terminal.ansiBrightBlack"], "ansiBrightRed": ["terminal.ansiBrightRed"], "ansiBrightGreen": ["terminal.ansiBrightGreen"], - "ansiBrightYellow": ["terminal.ansiBrightYellow"], "ansiBrightBlue": ["terminal.ansiBrightBlue"], "ansiBrightMagenta": ["terminal.ansiBrightMagenta"], - "ansiBrightCyan": ["terminal.ansiBrightCyan"], "ansiBrightWhite": ["terminal.ansiBrightWhite"] -}; - -exports.update = function (srcName, destName) { - try { - console.log('reading ', srcName); - let result = {}; - let plistContent = fs.readFileSync(srcName).toString(); - let theme = plist.parse(plistContent); - let settings = theme.settings; - if (Array.isArray(settings)) { - let colorMap = {}; - for (let entry of settings) { - let scope = entry.scope; - if (scope) { - let parts = scope.split(',').map(p => p.trim()); - if (parts.length > 1) { - entry.scope = parts; - } - } else { - var entrySettings = entry.settings; - for (let entry in entrySettings) { - let mapping = mappings[entry]; - if (mapping) { - for (let newKey of mapping) { - colorMap[newKey] = entrySettings[entry]; - } - if (entry !== 'foreground' && entry !== 'background') { - delete entrySettings[entry]; - } - } - } - - } - } - result.name = theme.name; - result.tokenColors = settings; - result.colors = colorMap; - } - fs.writeFileSync(destName, JSON.stringify(result, null, '\t')); - } catch (e) { - console.log(e); - } -}; - -if (path.basename(process.argv[1]) === 'update-theme.js') { - exports.update(process.argv[2], process.argv[3]); -} diff --git a/build/package.json b/build/package.json index 20fc702f..d1d0b42e 100644 --- a/build/package.json +++ b/build/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { + "@azure/cosmos": "^3.9.3", "@types/ansi-colors": "^3.2.0", "@types/azure": "0.9.19", "@types/debounce": "^1.0.0", @@ -16,50 +17,41 @@ "@types/gulp-json-editor": "^2.2.31", "@types/gulp-rename": "^0.0.33", "@types/gulp-sourcemaps": "^0.0.32", - "@types/gulp-uglify": "^3.0.5", "@types/mime": "0.0.29", "@types/minimatch": "^3.0.3", - "@types/minimist": "^1.2.0", - "@types/mocha": "2.2.39", - "@types/node": "^10.14.8", + "@types/minimist": "^1.2.1", + "@types/mocha": "^8.2.0", + "@types/node": "^12.19.9", + "@types/plist": "^3.0.2", "@types/pump": "^1.0.1", "@types/request": "^2.47.0", - "@types/rimraf": "^2.0.2", - "@types/terser": "^3.12.0", + "@types/rimraf": "^2.0.4", "@types/through": "^0.0.29", "@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", + "@typescript-eslint/parser": "^3.3.0", "applicationinsights": "1.0.8", "azure-storage": "^2.1.0", + "commander": "^7.0.0", "electron-osx-sign": "^0.4.16", - "github-releases": "^0.4.1", - "gulp-azure-storage": "^0.11.1", - "gulp-bom": "^1.0.0", - "gulp-gzip": "^1.4.2", - "gulp-sourcemaps": "^1.11.0", - "gulp-uglify": "^3.0.0", + "esbuild": "^0.8.30", + "fs-extra": "^9.1.0", "iconv-lite-umd": "0.6.8", "jsonc-parser": "^2.3.0", - "mime": "^1.3.4", - "minimatch": "3.0.4", - "minimist": "^1.2.3", - "request": "^2.85.0", - "terser": "4.3.8", - "typescript": "^4.2.0-dev.20201119", + "mime": "^1.4.1", + "mkdirp": "^1.0.4", + "plist": "^3.0.1", + "source-map": "0.6.1", + "typescript": "4.2.0-dev.20201207", "vsce": "1.48.0", - "vscode-telemetry-extractor": "^1.6.0", - "xml2js": "^0.4.17" + "vscode-universal": "deepak1556/universal#61454d96223b774c53cda10f72c2098c0ce02d58" }, "scripts": { "compile": "tsc -p tsconfig.build.json", "watch": "tsc -p tsconfig.build.json --watch", - "postinstall": "npm run compile", "npmCheckJs": "tsc --noEmit" }, - "dependencies": { - "@azure/cosmos": "^3.9.3" - } + "dependencies": {} } diff --git a/build/yarn.lock b/build/yarn.lock index 17c5c201..d958ed21 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -19,42 +19,12 @@ universal-user-agent "^6.0.0" uuid "^8.3.0" -"@dsherret/to-absolute-glob@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@dsherret/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1f6475dc8bd974cea07a2daf3864b317b1dd332c" - integrity sha1-H2R13IvZdM6gei2vOGSzF7HdMyw= +"@malept/cross-spawn-promise@^1.1.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz#504af200af6b98e198bce768bc1730c6936ae01d" + integrity sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ== dependencies: - is-absolute "^1.0.0" - is-negated-glob "^1.0.0" - -"@gulp-sourcemaps/map-sources@1.X": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz#890ae7c5d8c877f6d384860215ace9d7ec945bda" - integrity sha1-iQrnxdjId/bThIYCFazp1+yUW9o= - dependencies: - normalize-path "^2.0.1" - through2 "^2.0.3" - -"@nodelib/fs.scandir@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.1.tgz#7fa8fed654939e1a39753d286b48b4836d00e0eb" - integrity sha512-NT/skIZjgotDSiXs0WqYhgcuBKhUMgfekCmCGtkUAiLqZdOnrdjmZr9wRl3ll64J9NF79uZ4fk16Dx0yMc/Xbg== - dependencies: - "@nodelib/fs.stat" "2.0.1" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.1", "@nodelib/fs.stat@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.1.tgz#814f71b1167390cfcb6a6b3d9cdeb0951a192c14" - integrity sha512-+RqhBlLn6YRBGOIoVYthsG0J9dfpO79eJyN7BYBkZJtfqrBwf2KK+rD/M/yjZR6WBmIhAgOV7S60eCgaSWtbFw== - -"@nodelib/fs.walk@^1.2.1": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.2.tgz#6a6450c5e17012abd81450eb74949a4d970d2807" - integrity sha512-J/DR3+W12uCzAJkw7niXDcqcKBg6+5G5Q/ZpThpGNzAUz70eOR6RV4XnnSN01qHZiVl0eavoxJsBypQoKsV2QQ== - dependencies: - "@nodelib/fs.scandir" "2.1.1" - fastq "^1.6.0" + cross-spawn "^7.0.1" "@types/ansi-colors@^3.2.0": version "3.2.0" @@ -86,7 +56,7 @@ resolved "https://registry.yarnpkg.com/@types/debounce/-/debounce-1.0.0.tgz#417560200331e1bb84d72da85391102c2fcd61b7" integrity sha1-QXVgIAMx4buE1y2oU5EQLC/NYbc= -"@types/debug@^4.1.4": +"@types/debug@^4.1.4", "@types/debug@^4.1.5": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd" integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ== @@ -126,6 +96,13 @@ dependencies: "@types/node" "*" +"@types/fs-extra@^9.0.6": + version "9.0.6" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.6.tgz#488e56b77299899a608b8269719c1d133027a6ab" + integrity sha512-ecNRHw4clCkowNOBJH1e77nvbPxHYnWIXMv1IAoG/9+MYGkgoyr3Ppxr7XYFNL41V422EDhyV4/4SSK8L2mlig== + dependencies: + "@types/node" "*" + "@types/glob-stream@*": version "6.1.0" resolved "https://registry.yarnpkg.com/@types/glob-stream/-/glob-stream-6.1.0.tgz#7ede8a33e59140534f8d8adfb8ac9edfb31897bc" @@ -188,14 +165,6 @@ dependencies: "@types/node" "*" -"@types/gulp-uglify@^3.0.5": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@types/gulp-uglify/-/gulp-uglify-3.0.5.tgz#ddcbbb6bd15a84b8a6c5e2218c2efba98102d135" - integrity sha512-LD2b6gCPugrKI1W188nIp0gm+cAnGGwaTFpPdeZYVXwPHdoCQloy3du0JR62MeMjAwUwlcOb+SKYT6Qgw7yBiA== - dependencies: - "@types/node" "*" - "@types/uglify-js" "^2" - "@types/gulp@^4.0.5": version "4.0.5" resolved "https://registry.yarnpkg.com/@types/gulp/-/gulp-4.0.5.tgz#f5f498d5bf9538364792de22490a12c0e6bc5eb4" @@ -225,25 +194,38 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== -"@types/minimist@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" - integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= +"@types/minimist@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256" + integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg== -"@types/mocha@2.2.39": - version "2.2.39" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.39.tgz#f68d63db8b69c38e9558b4073525cf96c4f7a829" - integrity sha1-9o1j24tpw46VWLQHNSXPlsT3qCk= +"@types/mocha@^8.2.0": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.0.tgz#3eb56d13a1de1d347ecb1957c6860c911704bc44" + integrity sha512-/Sge3BymXo4lKc31C8OINJgXLaw+7vL1/L1pGiBNpGrBiT8FQiaFpSYV0uhTaG4y78vcMBTMFsWaHDvuD+xGzQ== "@types/node@*": version "8.0.51" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.51.tgz#b31d716fb8d58eeb95c068a039b9b6292817d5fb" integrity sha512-El3+WJk2D/ppWNd2X05aiP5l2k4EwF7KwheknQZls+I26eSICoWRhRIJ56jGgw2dqNGQ5LtNajmBU2ajS28EvQ== -"@types/node@^10.14.8": - version "10.14.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.13.tgz#ac786d623860adf39a3f51d629480aacd6a6eec7" - integrity sha512-yN/FNNW1UYsRR1wwAoyOwqvDuLDtVXnaJTZ898XIw/Q5cCaeVAlVwvsmXLX5PuiScBYwZsZU4JYSHB3TvfdwvQ== +"@types/node@^12.19.9": + version "12.19.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" + integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== + +"@types/node@^14.14.21": + version "14.14.22" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.22.tgz#0d29f382472c4ccf3bd96ff0ce47daf5b7b84b18" + integrity sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw== + +"@types/plist@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/plist/-/plist-3.0.2.tgz#61b3727bba0f5c462fe333542534a0c3e19ccb01" + integrity sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw== + dependencies: + "@types/node" "*" + xmlbuilder ">=11.0.1" "@types/pump@^1.0.1": version "1.0.1" @@ -262,21 +244,14 @@ "@types/node" "*" "@types/tough-cookie" "*" -"@types/rimraf@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-2.0.2.tgz#7f0fc3cf0ff0ad2a99bb723ae1764f30acaf8b6e" - integrity sha512-Hm/bnWq0TCy7jmjeN5bKYij9vw5GrDFWME4IuxV08278NtU/VdGbzsBohcCUJ7+QMqmUq5hpRKB39HeQWJjztQ== +"@types/rimraf@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-2.0.4.tgz#403887b0b53c6100a6c35d2ab24f6ccc042fec46" + integrity sha512-8gBudvllD2A/c0CcEX/BivIDorHFt5UI5m46TsNj8DjWCCTTZT74kEe4g+QsY7P/B9WdO98d82zZgXO/RQzu2Q== dependencies: "@types/glob" "*" "@types/node" "*" -"@types/terser@^3.12.0": - version "3.12.0" - resolved "https://registry.yarnpkg.com/@types/terser/-/terser-3.12.0.tgz#25e020fe9a7a6ae92ce46261f00ced67de6c12ac" - integrity sha512-J0Wy8A7ULEqVJftkWhrXZbH0iBk4tYuTj0gBiiveKaY9deNi6cCsxl0ApJ27ojqwYv51bvEw85lOb8Wt4ng9zA== - dependencies: - terser "*" - "@types/through2@^2.0.34": version "2.0.34" resolved "https://registry.yarnpkg.com/@types/through2/-/through2-2.0.34.tgz#9c2a259a238dace2a05a2f8e94b786961bc27ac4" @@ -296,13 +271,6 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.2.tgz#e0d481d8bb282ad8a8c9e100ceb72c995fb5e709" integrity sha512-vOVmaruQG5EatOU/jM6yU2uCp3Lz6mK1P5Ztu4iJjfM4SVHU9XYktPUQtKlIXuahqXHdEyUarMrBEwg5Cwu+bA== -"@types/uglify-js@^2": - version "2.6.31" - resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-2.6.31.tgz#c694755eeb6a1bb9f8f321f3ec37cc22ca4c4f6b" - integrity sha512-LjcyGt6CHsgZ0AoofnMwhyxo9hUqz2mgl6IcF+S8B1zdSTxHAvTO/1RPvBAHG3C1ZeAc+AoWA5mb3lDJKtM9Zg== - dependencies: - source-map "^0.6.1" - "@types/underscore@^1.8.9": version "1.8.9" resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.8.9.tgz#fef41f800cd23db1b4f262ddefe49cd952d82323" @@ -343,14 +311,16 @@ 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== +"@typescript-eslint/experimental-utils@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz#e179ffc81a80ebcae2ea04e0332f8b251345a686" + integrity sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.14.0" + "@typescript-eslint/types" "3.10.1" + "@typescript-eslint/typescript-estree" "3.10.1" eslint-scope "^5.0.0" + eslint-utils "^2.0.0" "@typescript-eslint/experimental-utils@~2.13.0": version "2.13.0" @@ -361,16 +331,22 @@ "@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== +"@typescript-eslint/parser@^3.3.0": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.10.1.tgz#1883858e83e8b442627e1ac6f408925211155467" + integrity sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw== dependencies: "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "2.14.0" - "@typescript-eslint/typescript-estree" "2.14.0" + "@typescript-eslint/experimental-utils" "3.10.1" + "@typescript-eslint/types" "3.10.1" + "@typescript-eslint/typescript-estree" "3.10.1" eslint-visitor-keys "^1.1.0" +"@typescript-eslint/types@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727" + integrity sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ== + "@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" @@ -384,46 +360,26 @@ 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== +"@typescript-eslint/typescript-estree@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz#fd0061cc38add4fad45136d654408569f365b853" + integrity sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w== dependencies: + "@typescript-eslint/types" "3.10.1" + "@typescript-eslint/visitor-keys" "3.10.1" 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" + lodash "^4.17.15" + semver "^7.3.2" tsutils "^3.17.1" -acorn@4.X: - version "4.0.13" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" - integrity sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c= - -agent-base@5: - version "5.1.1" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" - integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g== - -ajv@^4.9.1: - version "4.11.8" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" - integrity sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY= +"@typescript-eslint/visitor-keys@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz#cd4274773e3eb63b2e870ac602274487ecd1e931" + integrity sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ== dependencies: - co "^4.6.0" - json-stable-stringify "^1.0.1" - -ajv@^5.1.0: - version "5.5.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" - integrity sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU= - dependencies: - co "^4.6.0" - fast-deep-equal "^1.0.0" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.3.0" + eslint-visitor-keys "^1.1.0" ajv@^6.12.3: version "6.12.6" @@ -435,59 +391,6 @@ ajv@^6.12.3: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ansi-colors@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-1.1.0.tgz#6374b4dd5d4718ff3ce27a671a3b1cad077132a9" - integrity sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA== - dependencies: - ansi-wrap "^0.1.0" - -ansi-gray@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251" - integrity sha1-KWLPVOyXksSFEKPetSRDaGHvclE= - dependencies: - ansi-wrap "0.1.0" - -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@^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" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - -ansi-styles@^4.0.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -ansi-wrap@0.1.0, ansi-wrap@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" - integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= - -any-promise@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" - integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= - -append-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/append-buffer/-/append-buffer-1.0.2.tgz#d8220cf466081525efea50614f3de6514dfa58f1" - integrity sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE= - dependencies: - buffer-equal "^1.0.0" - applicationinsights@1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.8.tgz#db6e3d983cf9f9405fe1ee5ba30ac6e1914537b5" @@ -504,124 +407,51 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= - -array-back@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" - integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== - -array-differ@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" - integrity sha1-7/UuN1gknTO+QCuLuOVkuytdQDE= - -array-differ@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" - integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== - -array-each@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f" - integrity sha1-p5SvDAWrF1KEbudTofIRoFugxE8= - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -array-uniq@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= - -arrify@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" - integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== +asar@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/asar/-/asar-3.0.3.tgz#1fef03c2d6d2de0cbad138788e4f7ae03b129c7b" + integrity sha512-k7zd+KoR+n8pl71PvgElcoKHrVNiSXtw7odKbyNpmgKe7EGRF9Pnu3uLOukD37EvavKwVFxOUpqXTIZC5B5Pmw== + dependencies: + chromium-pickle-js "^0.2.0" + commander "^5.0.0" + glob "^7.1.6" + minimatch "^3.0.4" + optionalDependencies: + "@types/glob" "^7.1.1" asn1@~0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" - integrity sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y= + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= -assert-plus@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" - integrity sha1-104bh+ev/A24qttwIfP+SBAasjQ= - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -atob@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -aws-sign2@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" - integrity sha1-FDQt0428yU0OW4fXY81jYSwOeU8= +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= -aws4@^1.2.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" - integrity sha1-g+9cqGCysy5KDe7e6MdxudtXRx4= - -aws4@^1.6.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289" - integrity sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w== - aws4@^1.8.0: - version "1.10.1" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.1.tgz#e1e82e4f3e999e2cfd61b161280d16a111f86428" - integrity sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA== + version "1.11.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== azure-storage@^2.1.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/azure-storage/-/azure-storage-2.6.0.tgz#84747ee54a4bd194bb960f89f3eff89d67acf1cf" - integrity sha1-hHR+5UpL0ZS7lg+J8+/4nWes8c8= - dependencies: - browserify-mime "~1.2.9" - extend "~1.2.1" - json-edm-parser "0.1.2" - md5.js "1.3.4" - readable-stream "~2.0.0" - request "~2.81.0" - underscore "~1.8.3" - uuid "^3.0.0" - validator "~3.35.0" - xml2js "0.2.7" - xmlbuilder "0.4.3" - -azure-storage@^2.10.2: version "2.10.3" resolved "https://registry.yarnpkg.com/azure-storage/-/azure-storage-2.10.3.tgz#c5966bf929d87587d78f6847040ea9a4b1d4a50a" integrity sha512-IGLs5Xj6kO8Ii90KerQrrwuJKexLgSwYC4oLWmc11mzKe7Jt2E5IVg+ZQ8K53YWZACtVTMBNO3iGuA+4ipjJxQ== @@ -649,17 +479,12 @@ base64-js@^1.2.3: integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== bcrypt-pbkdf@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" - integrity sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40= + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= dependencies: tweetnacl "^0.14.3" -beeper@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809" - integrity sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak= - bluebird@^3.5.0: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" @@ -670,27 +495,6 @@ boolbase@~1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= -boom@2.x.x: - version "2.10.1" - resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" - integrity sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8= - dependencies: - hoek "2.x.x" - -boom@4.x.x: - version "4.3.1" - resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" - integrity sha1-T4owBctKfjiJ90kDD9JbluAdLjE= - dependencies: - hoek "4.x.x" - -boom@5.x.x: - version "5.2.0" - resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" - integrity sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw== - dependencies: - hoek "4.x.x" - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -699,13 +503,6 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - browserify-mime@~1.2.9: version "1.2.9" resolved "https://registry.yarnpkg.com/browserify-mime/-/browserify-mime-1.2.9.tgz#aeb1af28de6c0d7a6a2ce40adb68ff18422af31f" @@ -729,7 +526,7 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= -buffer-equal@^1.0.0: +buffer-equal@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" integrity sha1-WWFrSYME1Var1GaWayLu2j7KX74= @@ -739,37 +536,11 @@ buffer-fill@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= -buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - -bytes@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== - -camelcase@^5.0.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -chalk@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.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" @@ -782,94 +553,15 @@ cheerio@^1.0.0-rc.1: lodash "^4.15.0" parse5 "^3.0.1" -cliui@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" - integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^6.2.0" +chromium-pickle-js@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz#04a106672c18b085ab774d983dfa3ea138f22205" + integrity sha1-BKEGZywYsIWrd02YPfo+oTjyIgU= -clone-buffer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" - integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= - -clone-stats@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1" - integrity sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE= - -clone-stats@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" - integrity sha1-s3gt/4u1R04Yuba/D9/ngvh3doA= - -clone@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" - integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= - -clone@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" - integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= - -cloneable-readable@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.1.3.tgz#120a00cb053bfb63a222e709f9683ea2e11d8cec" - integrity sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ== - dependencies: - inherits "^2.0.1" - process-nextick-args "^2.0.0" - readable-stream "^2.3.5" - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= - -code-block-writer@9.4.1: - version "9.4.1" - resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-9.4.1.tgz#1448fca79dfc7a3649000f4c85be6bc770604c4c" - integrity sha512-LHAB+DL4YZDcwK8y/kAxZ0Lf/ncwLh/Ux4cTVWbPwIdrf1gPxXiPcwpz8r8/KqXu1aD+Raz46EOxDjFlbyO6bA== - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-support@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" - integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== - -colors@^1.1.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.1.tgz#f4a3d302976aaf042356ba1ade3b1a2c62d9d794" - integrity sha512-s8+wktIuDSLffCywiwSxQOMqtPxML11a/dtHE17tMn4B1MSWw/C22EKf7M2KGUBcDaVFEGT+S8N02geDXeuNKg== - -combined-stream@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" - integrity sha1-cj599ugBrFYTETp+RFqbactjKBg= - dependencies: - delayed-stream "~1.0.0" - -combined-stream@^1.0.5, combined-stream@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" - integrity sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk= - dependencies: - delayed-stream "~1.0.0" +colors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" + integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8" @@ -878,26 +570,28 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -command-line-args@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.1.1.tgz#88e793e5bb3ceb30754a86863f0401ac92fd369a" - integrity sha512-hL/eG8lrll1Qy1ezvkant+trihbGnaKaeEjj6Scyr3DN+RC7iQ5Rz84IeLERfAWDGo0HBSNAakczwgCilDXnWg== +commander@2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" + integrity sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q= dependencies: - array-back "^3.0.1" - find-replace "^3.0.0" - lodash.camelcase "^4.3.0" - typical "^4.0.0" - -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== + graceful-readlink ">= 1.0.0" 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@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== + +commander@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.0.0.tgz#3e2bbfd8bb6724760980988fb5b22b7ee6b71ab2" + integrity sha512-ovx/7NkTrnPuIV8sqk/GjUIIM1+iUQeqA3ye2VNpq9sVoiZsooObWlQy+OPWGI17GDaEoybuAGJm6U8yC077BA== + compare-version@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/compare-version/-/compare-version-0.1.2.tgz#0162ec2d9351f5ddd59a9202cba935366a725080" @@ -908,38 +602,19 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -convert-source-map@1.X: - version "1.6.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" - integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== - dependencies: - safe-buffer "~5.1.1" - -convert-source-map@^1.5.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" - 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" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= -cryptiles@2.x.x: - version "2.0.5" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" - integrity sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g= +cross-spawn@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== dependencies: - boom "2.x.x" - -cryptiles@3.x.x: - version "3.1.2" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" - integrity sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4= - dependencies: - boom "5.x.x" + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" css-select@~1.2.0: version "1.2.0" @@ -956,16 +631,6 @@ css-what@2.1: resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.2.tgz#c0876d9d0480927d7d4920dcd72af3595649554d" integrity sha512-wan8dMWQ0GUeF7DGEPVjhHemVW/vy6xUYmFzRY8RYqgA0JtXC9rJmbScBjqSu6dg9q0lwPQy6ZAmJVr3PPTvqQ== -css@2.X: - version "2.2.4" - resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929" - integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw== - dependencies: - inherits "^2.0.3" - source-map "^0.6.1" - source-map-resolve "^0.5.2" - urix "^0.1.0" - dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -973,63 +638,20 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -dateformat@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.2.0.tgz#4065e2013cf9fb916ddfd82efb506ad4c6769062" - integrity sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI= - -debug-fabulous@0.0.X: - version "0.0.4" - resolved "https://registry.yarnpkg.com/debug-fabulous/-/debug-fabulous-0.0.4.tgz#fa071c5d87484685424807421ca4b16b0b1a0763" - integrity sha1-+gccXYdIRoVCSAdCHKSxawsaB2M= - dependencies: - debug "2.X" - lazy-debug-legacy "0.0.X" - object-assign "4.1.0" - -debug@2.X, debug@^2.6.8: +debug@^2.6.8: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@4: - version "4.1.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" - integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== - dependencies: - ms "^2.1.1" - -debug@^4.1.1: +debug@^4.1.1, debug@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== dependencies: ms "2.1.2" -decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= - -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== - dependencies: - object-keys "^1.0.12" - -delayed-stream@0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-0.0.6.tgz#a2646cb7ec3d5d7774614670a7a65de0c173edbc" - integrity sha1-omRst+w9XXd0YUZwp6Zd4MFz7bw= - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -1040,11 +662,6 @@ denodeify@^1.2.1: resolved "https://registry.yarnpkg.com/denodeify/-/denodeify-1.2.1.tgz#3a36287f5034e699e7577901052c2e6c94251631" integrity sha1-OjYof1A05pnnV3kBBSwubJQlFjE= -detect-newline@2.X: - version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" - integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= - 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" @@ -1057,12 +674,15 @@ diagnostic-channel@0.2.0: dependencies: semver "^5.3.0" -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== +dir-compare@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/dir-compare/-/dir-compare-2.4.0.tgz#785c41dc5f645b34343a4eafc50b79bac7f11631" + integrity sha512-l9hmu8x/rjVC9Z2zmGzkhOEowZvW7pmYws5CWHutg8u1JgvsKWMx7Q/UODeu4djLZ4FgW5besw5yvMQnBHzuCA== dependencies: - path-type "^4.0.0" + buffer-equal "1.0.0" + colors "1.0.3" + commander "2.9.0" + minimatch "3.0.4" dom-serializer@0, dom-serializer@~0.1.0: version "0.1.0" @@ -1110,34 +730,13 @@ domutils@^1.5.1: dom-serializer "0" domelementtype "1" -duplexer2@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.0.2.tgz#c614dcf67e2fb14995a91711e5a617e8a60a31db" - integrity sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds= - dependencies: - readable-stream "~1.1.9" - -duplexer@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" - integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== - -duplexify@^3.6.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" - integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== - dependencies: - end-of-stream "^1.0.0" - inherits "^2.0.1" - readable-stream "^2.0.0" - stream-shift "^1.0.0" - ecc-jsbn@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" - integrity sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU= + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= dependencies: jsbn "~0.1.0" + safer-buffer "^2.1.0" electron-osx-sign@^0.4.16: version "0.4.16" @@ -1151,54 +750,15 @@ electron-osx-sign@^0.4.16: minimist "^1.2.0" plist "^3.0.1" -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== - -end-of-stream@^1.0.0, end-of-stream@^1.1.0: - 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" - entities@^1.1.1, entities@~1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== -es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1: - version "1.18.0-next.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" - integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" - is-negative-zero "^2.0.0" - is-regex "^1.1.1" - object-inspect "^1.8.0" - object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -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= +esbuild@^0.8.30: + version "0.8.30" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.8.30.tgz#3d057ff9ffe6d5d30bccb0afe8cc92a2e69622d3" + integrity sha512-gCJQYUMO9QNrfpNOIiCnFoX41nWiPFCvURBQF+qWckyJ7gmw2xCShdKCXvS+RZcQ5krcxEOLIkzujqclePKhfw== eslint-scope@^5.0.0: version "5.0.0" @@ -1208,6 +768,13 @@ eslint-scope@^5.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" +eslint-utils@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== + dependencies: + eslint-visitor-keys "^1.1.0" + 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" @@ -1225,100 +792,31 @@ estraverse@^4.1.0, estraverse@^4.1.1: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== -event-stream@3.3.4: - version "3.3.4" - resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" - integrity sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE= - dependencies: - duplexer "~0.1.1" - from "~0" - map-stream "~0.1.0" - pause-stream "0.0.11" - split "0.3" - stream-combiner "~0.0.4" - through "~2.3.1" - -extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -extend@^3.0.0, extend@^3.0.2, extend@~3.0.2: +extend@^3.0.2, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== -extend@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/extend/-/extend-1.2.1.tgz#a0f5fd6cfc83a5fe49ef698d60ec8a624dd4576c" - integrity sha1-oPX9bPyDpf5J72mNYOyKYk3UV2w= - -extend@~3.0.0, extend@~3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" - integrity sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ= - -extsprintf@1.3.0, extsprintf@^1.2.0: +extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= -fancy-log@^1.1.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.2.tgz#f41125e3d84f2e7d89a43d06d958c8f78be16be1" - integrity sha1-9BEl49hPLn2JpD0G2VjI94vha+E= - dependencies: - ansi-gray "^0.1.1" - color-support "^1.1.3" - time-stamp "^1.0.0" - -fancy-log@^1.3.2: - version "1.3.3" - resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.3.tgz#dbc19154f558690150a23953a0adbd035be45fc7" - integrity sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw== - dependencies: - ansi-gray "^0.1.1" - color-support "^1.1.3" - parse-node-version "^1.0.0" - time-stamp "^1.0.0" - -fast-deep-equal@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" - integrity sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ= +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= fast-deep-equal@^3.1.1: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.0.3: - version "3.0.4" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.0.4.tgz#d484a41005cb6faeb399b951fd1bd70ddaebb602" - integrity sha512-wkIbV6qg37xTJwqSsdnIphL1e+LaGz4AIQqr00mIubMaEhv1/HEmJ0uuCGZRNRUkZZmOB5mJKO0ZUTVq+SxMQg== - dependencies: - "@nodelib/fs.stat" "^2.0.1" - "@nodelib/fs.walk" "^1.2.1" - glob-parent "^5.0.0" - is-glob "^4.0.1" - merge2 "^1.2.3" - micromatch "^4.0.2" - fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fastq@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.6.0.tgz#4ec8a38f4ac25f21492673adb7eae9cfef47d1c2" - integrity sha512-jmxqQ3Z/nXoeyDmWAzF9kH1aGZSis6e/SbfPmJpUnyZ0ogr6iscHQaml4wsEepEWSdtmpy+eVXmCRIMpxaXqOA== - dependencies: - reusify "^1.0.0" - fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" @@ -1326,59 +824,11 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -find-replace@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" - integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== - dependencies: - array-back "^3.0.1" - -find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -flush-write-stream@^1.0.2: - version "1.1.1" - resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" - integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== - dependencies: - inherits "^2.0.3" - readable-stream "^2.3.6" - forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= -form-data@~2.1.1: - version "2.1.4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" - integrity sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE= - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.5" - mime-types "^2.1.12" - -form-data@~2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" - integrity sha1-SXBJi+YEwgwAXU9cI67NIda0kJk= - dependencies: - asynckit "^0.4.0" - combined-stream "1.0.6" - mime-types "^2.1.12" - form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -1388,43 +838,21 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" -from@~0: - version "0.1.7" - resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" - integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4= - -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== +fs-extra@^9.0.1, fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== dependencies: + at-least-node "^1.0.0" graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" - -fs-mkdirp-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz#0b7815fc3201c6a69e14db98ce098c16935259eb" - integrity sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes= - dependencies: - graceful-fs "^4.1.11" - through2 "^2.0.3" + jsonfile "^6.0.1" + universalify "^2.0.0" 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= -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -get-caller-file@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -1432,47 +860,6 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -github-releases@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/github-releases/-/github-releases-0.4.1.tgz#4a13bdf85c4161344271db3d81db08e7379102ff" - integrity sha1-ShO9+FxBYTRCcds9gdsI5zeRAv8= - dependencies: - minimatch "3.0.4" - optimist "0.6.1" - prettyjson "1.2.1" - request "2.81.0" - -glob-parent@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= - dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" - -glob-parent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.0.0.tgz#1dc99f0f39b006d3e92c2c284068382f0c20e954" - integrity sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg== - dependencies: - is-glob "^4.0.1" - -glob-stream@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-6.1.0.tgz#7045c99413b3eb94888d83ab46d0b404cc7bdde4" - integrity sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ= - dependencies: - extend "^3.0.0" - glob "^7.1.1" - glob-parent "^3.1.0" - is-negated-glob "^1.0.0" - ordered-read-streams "^1.0.0" - pumpify "^1.3.5" - readable-stream "^2.1.5" - remove-trailing-separator "^1.0.1" - to-absolute-glob "^2.0.0" - unique-stream "^2.0.2" - glob@^7.0.6: version "7.1.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" @@ -1485,7 +872,7 @@ glob@^7.0.6: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.1, glob@^7.1.6: +glob@^7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -1497,180 +884,21 @@ glob@^7.1.1, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.3: - version "7.1.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" - integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== - 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" - integrity sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A== - dependencies: - "@types/glob" "^7.1.1" - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.0.3" - glob "^7.1.3" - ignore "^5.1.1" - merge2 "^1.2.3" - slash "^3.0.0" - -glogg@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/glogg/-/glogg-1.0.1.tgz#dcf758e44789cc3f3d32c1f3562a3676e6a34810" - integrity sha512-ynYqXLoluBKf9XGR1gA59yEJisIL7YHEH4xr3ZziHB5/yl4qWfaK8Js9jGe6gBGCSCKVqiyO30WnRZADvemUNw== - dependencies: - sparkles "^1.0.0" - -graceful-fs@4.X: - version "4.1.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" - integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg= - -graceful-fs@^4.0.0, graceful-fs@^4.1.11: +graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== -graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.0.tgz#8d8fdc73977cb04104721cb53666c1ca64cd328b" - integrity sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg== - -gulp-azure-storage@^0.11.1: - version "0.11.1" - resolved "https://registry.yarnpkg.com/gulp-azure-storage/-/gulp-azure-storage-0.11.1.tgz#0e5f5d0f789da11206f1e5a9311a6cf7107877d7" - integrity sha512-csOwItwZV1P9GLsORVQy+CFwjYDdHNrBol89JlHdlhGx0fTgJBc1COTRZbjGRyRjgdUuVguo3YLl4ToJ10/SIQ== - dependencies: - azure-storage "^2.10.2" - delayed-stream "0.0.6" - event-stream "3.3.4" - mime "^1.3.4" - progress "^1.1.8" - queue "^3.0.10" - streamifier "^0.1.1" - vinyl "^2.2.0" - vinyl-fs "^3.0.3" - yargs "^15.3.0" - -gulp-bom@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/gulp-bom/-/gulp-bom-1.0.0.tgz#38a183a07187bd57a7922d37977441f379df2abf" - integrity sha1-OKGDoHGHvVenki03l3RB83nfKr8= - dependencies: - gulp-util "^3.0.0" - through2 "^2.0.0" - -gulp-gzip@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/gulp-gzip/-/gulp-gzip-1.4.2.tgz#0422a94014248655b5b1a9eea1c2abee1d4f4337" - integrity sha512-ZIxfkUwk2XmZPTT9pPHrHUQlZMyp9nPhg2sfoeN27mBGpi7OaHnOD+WCN41NXjfJQ69lV1nQ9LLm1hYxx4h3UQ== - dependencies: - ansi-colors "^1.0.1" - bytes "^3.0.0" - fancy-log "^1.3.2" - plugin-error "^1.0.0" - stream-to-array "^2.3.0" - through2 "^2.0.3" - -gulp-sourcemaps@^1.11.0: - version "1.12.1" - resolved "https://registry.yarnpkg.com/gulp-sourcemaps/-/gulp-sourcemaps-1.12.1.tgz#b437d1f3d980cf26e81184823718ce15ae6597b6" - integrity sha1-tDfR89mAzyboEYSCNxjOFa5ll7Y= - dependencies: - "@gulp-sourcemaps/map-sources" "1.X" - acorn "4.X" - convert-source-map "1.X" - css "2.X" - debug-fabulous "0.0.X" - detect-newline "2.X" - graceful-fs "4.X" - source-map "~0.6.0" - strip-bom "2.X" - through2 "2.X" - vinyl "1.X" - -gulp-uglify@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/gulp-uglify/-/gulp-uglify-3.0.2.tgz#5f5b2e8337f879ca9dec971feb1b82a5a87850b0" - integrity sha512-gk1dhB74AkV2kzqPMQBLA3jPoIAPd/nlNzP2XMDSG8XZrqnlCiDGAqC+rZOumzFvB5zOphlFh6yr3lgcAb/OOg== - dependencies: - array-each "^1.0.1" - extend-shallow "^3.0.2" - gulplog "^1.0.0" - has-gulplog "^0.1.0" - isobject "^3.0.1" - make-error-cause "^1.1.1" - safe-buffer "^5.1.2" - through2 "^2.0.0" - uglify-js "^3.0.5" - vinyl-sourcemaps-apply "^0.2.0" - -gulp-util@^3.0.0: - version "3.0.8" - resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" - integrity sha1-AFTh50RQLifATBh8PsxQXdVLu08= - dependencies: - array-differ "^1.0.0" - array-uniq "^1.0.2" - beeper "^1.0.0" - chalk "^1.0.0" - dateformat "^2.0.0" - fancy-log "^1.1.0" - gulplog "^1.0.0" - has-gulplog "^0.1.0" - lodash._reescape "^3.0.0" - lodash._reevaluate "^3.0.0" - lodash._reinterpolate "^3.0.0" - lodash.template "^3.0.0" - minimist "^1.1.0" - multipipe "^0.1.2" - object-assign "^3.0.0" - replace-ext "0.0.1" - through2 "^2.0.0" - vinyl "^0.5.0" - -gulplog@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/gulplog/-/gulplog-1.0.0.tgz#e28c4d45d05ecbbed818363ce8f9c5926229ffe5" - integrity sha1-4oxNRdBey77YGDY86PnFkmIp/+U= - dependencies: - glogg "^1.0.0" - -har-schema@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" - integrity sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4= +"graceful-readlink@>= 1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" + integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= -har-validator@~4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" - integrity sha1-M0gdDxu/9gDdID11gSpqX7oALio= - dependencies: - ajv "^4.9.1" - har-schema "^1.0.5" - -har-validator@~5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" - integrity sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0= - dependencies: - ajv "^5.1.0" - har-schema "^2.0.0" - har-validator@~5.1.3: version "5.1.5" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" @@ -1679,69 +907,14 @@ har-validator@~5.1.3: ajv "^6.12.3" har-schema "^2.0.0" -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - -has-gulplog@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/has-gulplog/-/has-gulplog-0.1.0.tgz#6414c82913697da51590397dafb12f22967811ce" - integrity sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4= - dependencies: - sparkles "^1.0.0" - -has-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - hash-base@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" - integrity sha1-X8hoaEfs1zSZQDMZprCj8/auSRg= + version "3.1.0" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" + integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -hawk@~3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" - integrity sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ= - dependencies: - boom "2.x.x" - cryptiles "2.x.x" - hoek "2.x.x" - sntp "1.x.x" - -hawk@~6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" - integrity sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ== - dependencies: - boom "4.x.x" - cryptiles "3.x.x" - hoek "4.x.x" - sntp "2.x.x" - -hoek@2.x.x: - version "2.16.3" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" - integrity sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0= - -hoek@4.x.x: - version "4.2.1" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" - integrity sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA== + inherits "^2.0.4" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" htmlparser2@^3.9.1: version "3.10.0" @@ -1755,15 +928,6 @@ htmlparser2@^3.9.1: inherits "^2.0.1" readable-stream "^3.0.6" -http-signature@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" - integrity sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8= - dependencies: - assert-plus "^0.2.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -1773,24 +937,11 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" -https-proxy-agent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b" - integrity sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg== - dependencies: - agent-base "5" - debug "4" - iconv-lite-umd@0.6.8: version "0.6.8" resolved "https://registry.yarnpkg.com/iconv-lite-umd/-/iconv-lite-umd-0.6.8.tgz#5ad310ec126b260621471a2d586f7f37b9958ec0" integrity sha512-zvXJ5gSwMC9JD3wDzH8CoZGc1pbiJn12Tqjk8BXYCnYz3hYL5GRjHW8LEykjXhV9WgNGI4rgpgHcbIiBfrRq6A== -ignore@^5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.2.tgz#e28e584d43ad7e92f96995019cc43b9e1ac49558" - integrity sha512-vdqWBp7MyzdmHkkRWV5nY+PfGRbYbahfuvsBCh277tq+w9zyNi7h5CYJCK0kmzti9kU+O/cB7sE8HvKv6aXAKQ== - inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1799,63 +950,21 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@~2.0.0: +inherits@2, inherits@^2.0.1, inherits@^2.0.4, inherits@~2.0.1: 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.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -is-absolute@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" - integrity sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA== - dependencies: - is-relative "^1.0.0" - is-windows "^1.0.1" - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-callable@^1.1.4, is-callable@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" - integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== - -is-date-object@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^2.1.0, is-extglob@^2.1.1: +is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= -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@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= - dependencies: - is-extglob "^2.1.0" - is-glob@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" @@ -1863,81 +972,11 @@ is-glob@^4.0.1: dependencies: is-extglob "^2.1.1" -is-negated-glob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" - integrity sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI= - -is-negative-zero@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" - integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-regex@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" - integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== - dependencies: - has-symbols "^1.0.1" - -is-relative@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" - integrity sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA== - dependencies: - is-unc-path "^1.0.0" - -is-symbol@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" - integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== - dependencies: - has-symbols "^1.0.1" - is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= -is-unc-path@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d" - integrity sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ== - dependencies: - unc-path-regex "^0.1.2" - -is-utf8@^0.2.0, is-utf8@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= - -is-valid-glob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" - integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao= - -is-windows@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= - isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -1950,10 +989,10 @@ isbinaryfile@^3.0.2: dependencies: buffer-alloc "^1.2.0" -isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= isstream@~0.1.2: version "0.1.2" @@ -1977,11 +1016,6 @@ json-edm-parser@0.1.2: dependencies: jsonparse "~1.2.0" -json-schema-traverse@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" - integrity sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A= - json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -1992,18 +1026,6 @@ json-schema@0.2.3: resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= - -json-stable-stringify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" - integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8= - dependencies: - jsonify "~0.0.0" - 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" @@ -2014,18 +1036,15 @@ jsonc-parser@^2.3.0: resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.0.tgz#7c7fc988ee1486d35734faaaa866fadb00fa91ee" integrity sha512-b0EBt8SWFNnixVdvoR2ZtEGa9ZqLhbJnOjezn+WP+8kspFm+PFYDN8Z4Bc7pRlDjvuVcADSUkroIuTWWn/YiIA== -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" optionalDependencies: graceful-fs "^4.1.6" -jsonify@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" - integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= - jsonparse@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.2.0.tgz#5c0c5685107160e72fe7489bddea0b44c2bc67bd" @@ -2041,25 +1060,6 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -lazy-debug-legacy@0.0.X: - version "0.0.1" - resolved "https://registry.yarnpkg.com/lazy-debug-legacy/-/lazy-debug-legacy-0.0.1.tgz#537716c0776e4cf79e3ed1b621f7658c2911b1b1" - integrity sha1-U3cWwHduTPeePtG2IfdljCkRsbE= - -lazystream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" - integrity sha1-9plf4PggOS9hOWvolGJAe7dxaOQ= - dependencies: - readable-stream "^2.0.5" - -lead@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lead/-/lead-1.0.0.tgz#6f14f99a37be3a9dd784f5495690e5903466ee42" - integrity sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI= - dependencies: - flush-write-stream "^1.0.2" - linkify-it@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.0.3.tgz#d94a4648f9b1c179d64fa97291268bdb6ce9434f" @@ -2067,117 +1067,6 @@ linkify-it@^2.0.0: dependencies: uc.micro "^1.0.1" -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -lodash._basecopy@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" - integrity sha1-jaDmqHbPNEwK2KVIghEd08XHyjY= - -lodash._basetostring@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz#d1861d877f824a52f669832dcaf3ee15566a07d5" - integrity sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U= - -lodash._basevalues@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz#5b775762802bde3d3297503e26300820fdf661b7" - integrity sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc= - -lodash._getnative@^3.0.0: - version "3.9.1" - resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" - integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= - -lodash._isiterateecall@^3.0.0: - version "3.0.9" - resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" - integrity sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw= - -lodash._reescape@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._reescape/-/lodash._reescape-3.0.0.tgz#2b1d6f5dfe07c8a355753e5f27fac7f1cde1616a" - integrity sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo= - -lodash._reevaluate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz#58bc74c40664953ae0b124d806996daca431e2ed" - integrity sha1-WLx0xAZklTrgsSTYBpltrKQx4u0= - -lodash._reinterpolate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" - integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= - -lodash._root@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" - integrity sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI= - -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" - integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= - -lodash.escape@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-3.2.0.tgz#995ee0dc18c1b48cc92effae71a10aab5b487698" - integrity sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg= - dependencies: - lodash._root "^3.0.0" - -lodash.isarguments@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" - integrity sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo= - -lodash.isarray@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" - integrity sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U= - -lodash.keys@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" - integrity sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo= - dependencies: - lodash._getnative "^3.0.0" - lodash.isarguments "^3.0.0" - lodash.isarray "^3.0.0" - -lodash.restparam@^3.0.0: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU= - -lodash.template@^3.0.0: - version "3.6.2" - resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-3.6.2.tgz#f8cdecc6169a255be9098ae8b0c53d378931d14f" - integrity sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8= - dependencies: - lodash._basecopy "^3.0.0" - lodash._basetostring "^3.0.0" - lodash._basevalues "^3.0.0" - lodash._isiterateecall "^3.0.0" - lodash._reinterpolate "^3.0.0" - lodash.escape "^3.0.0" - lodash.keys "^3.0.0" - lodash.restparam "^3.0.0" - lodash.templatesettings "^3.0.0" - -lodash.templatesettings@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5" - integrity sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU= - dependencies: - 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" @@ -2188,22 +1077,17 @@ lodash@^4.15.0, lodash@^4.17.10: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== -make-error-cause@^1.1.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-1.2.2.tgz#df0388fcd0b37816dff0a5fb8108939777dcbc9d" - integrity sha1-3wOI/NCzeBbf8KX7gQiTl3fcvJ0= +lodash@^4.17.15: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== dependencies: - make-error "^1.2.0" - -make-error@^1.2.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" - integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g== - -map-stream@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" - integrity sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ= + yallist "^4.0.0" markdown-it@^8.3.1: version "8.4.2" @@ -2229,60 +1113,28 @@ mdurl@^1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= -merge2@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.3.tgz#7ee99dbd69bb6481689253f018488a1b902b0ed5" - integrity sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA== +mime-db@1.45.0: + version "1.45.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" + integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== -micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.28" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd" + integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ== dependencies: - braces "^3.0.1" - picomatch "^2.0.5" - -mime-db@1.44.0: - version "1.44.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" - integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== - -mime-db@~1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" - integrity sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE= - -mime-db@~1.33.0: - version "1.33.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" - integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== - -mime-types@^2.1.12, mime-types@~2.1.7: - version "2.1.17" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" - integrity sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo= - dependencies: - mime-db "~1.30.0" - -mime-types@~2.1.17: - version "2.1.18" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" - integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== - dependencies: - mime-db "~1.33.0" - -mime-types@~2.1.19: - version "2.1.27" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" - integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== - dependencies: - mime-db "1.44.0" + mime-db "1.45.0" mime@^1.3.4: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== +mime@^1.4.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + minimatch@3.0.4, minimatch@^3.0.3, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -2290,44 +1142,26 @@ minimatch@3.0.4, minimatch@^3.0.3, minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.3: +minimist@^1.2.0: version "1.2.3" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.3.tgz#3db5c0765545ab8637be71f333a104a965a9ca3f" integrity sha512-+bMdgqjMN/Z77a6NlY/I3U5LlRDbnmaAk6lDveAPKwSpcPM4tKAuYsvYF8xjhOPXhOYGe/73vVLVez5PW+jqhw== -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@2.1.2, ms@^2.1.1: +ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -multimatch@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-4.0.0.tgz#8c3c0f6e3e8449ada0af3dd29efb491a375191b3" - integrity sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ== - dependencies: - "@types/minimatch" "^3.0.3" - array-differ "^3.0.0" - array-union "^2.1.0" - arrify "^2.0.1" - minimatch "^3.0.4" - -multipipe@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/multipipe/-/multipipe-0.1.2.tgz#2a8f2ddf70eed564dff2d57f1e1a137d9f05078b" - integrity sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s= - dependencies: - duplexer2 "0.0.2" - mute-stream@~0.0.4: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -2343,20 +1177,6 @@ node-fetch@^2.6.0: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== -normalize-path@^2.0.1, normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= - dependencies: - remove-trailing-separator "^1.0.1" - -now-and-later@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.1.tgz#8e579c8685764a7cc02cb680380e94f43ccb1f7c" - integrity sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ== - dependencies: - once "^1.3.2" - nth-check@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" @@ -2364,68 +1184,18 @@ nth-check@~1.0.1: dependencies: boolbase "~1.0.0" -oauth-sign@~0.8.1, oauth-sign@~0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" - integrity sha1-Rqarfwrq2N6unsBWV4C31O/rnUM= - oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" - integrity sha1-ejs9DpgGPUP0wD8uiubNUahog6A= - -object-assign@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" - integrity sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I= - -object-inspect@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" - integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== - -object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.0.4, object.assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd" - integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.18.0-next.0" - has-symbols "^1.0.1" - object-keys "^1.1.1" - -once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: +once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= dependencies: wrappy "1" -optimist@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - -ordered-read-streams@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e" - integrity sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4= - dependencies: - readable-stream "^2.0.1" - os-homedir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" @@ -2444,30 +1214,6 @@ osenv@^0.1.3: os-homedir "^1.0.0" os-tmpdir "^1.0.0" -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -parse-node-version@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" - integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== - parse-semver@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/parse-semver/-/parse-semver-1.1.1.tgz#9a4afd6df063dc4826f93fba4a99cf223f666cb8" @@ -2482,53 +1228,26 @@ parse5@^3.0.1: dependencies: "@types/node" "*" -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - 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-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -pause-stream@0.0.11: - version "0.0.11" - resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" - integrity sha1-/lo0sMvOErWqaitAPuLnO2AvFEU= - dependencies: - through "~2.3" +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= -performance-now@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" - integrity sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU= - performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -picomatch@^2.0.5: - version "2.0.7" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" - integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== - plist@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.1.tgz#a9b931d17c304e8912ef0ba3bdd6182baf2e1f8c" @@ -2538,81 +1257,21 @@ plist@^3.0.1: xmlbuilder "^9.0.7" xmldom "0.1.x" -plugin-error@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c" - integrity sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA== - dependencies: - ansi-colors "^1.0.1" - arr-diff "^4.0.0" - arr-union "^3.1.0" - extend-shallow "^3.0.2" - -prettyjson@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/prettyjson/-/prettyjson-1.2.1.tgz#fcffab41d19cab4dfae5e575e64246619b12d289" - integrity sha1-/P+rQdGcq0365eV15kJGYZsS0ok= - dependencies: - colors "^1.1.2" - minimist "^1.2.0" - priorityqueuejs@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/priorityqueuejs/-/priorityqueuejs-1.0.0.tgz#2ee4f23c2560913e08c07ce5ccdd6de3df2c5af8" integrity sha1-LuTyPCVgkT4IwHzlzN1t498sWvg= -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== - process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" integrity sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M= -process-nextick-args@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" - integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== - -progress@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" - integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74= - -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - psl@^1.1.28: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== -pump@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" - integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pumpify@^1.3.5: - version "1.5.1" - resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" - integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== - dependencies: - duplexify "^3.6.0" - inherits "^2.0.3" - pump "^2.0.0" - -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -2623,28 +1282,11 @@ q@^1.0.1: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= -qs@~6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" - integrity sha1-E+JtKK1rD/qpExLNO/cI7TUecjM= - -qs@~6.5.1: - version "6.5.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" - integrity sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A== - qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== -queue@^3.0.10: - version "3.1.0" - resolved "https://registry.yarnpkg.com/queue/-/queue-3.1.0.tgz#6c49d01f009e2256788789f2bffac6b8b9990585" - integrity sha1-bEnQHwCeIlZ4h4nyv/rGuLmZBYU= - dependencies: - inherits "~2.0.0" - read@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" @@ -2652,32 +1294,6 @@ read@^1.0.7: dependencies: mute-stream "~0.0.4" -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.5, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.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@^2.1.5: - version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" - integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== - 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.6: version "3.0.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.0.6.tgz#351302e4c68b5abd6a2ed55376a7f9a25be3057a" @@ -2687,15 +1303,14 @@ readable-stream@^3.0.6: string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@~1.1.9: - version "1.1.14" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" - integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= +readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" readable-stream@~2.0.0: version "2.0.6" @@ -2709,94 +1324,6 @@ readable-stream@~2.0.0: string_decoder "~0.10.x" util-deprecate "~1.0.1" -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" - integrity sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ== - dependencies: - is-buffer "^1.1.5" - is-utf8 "^0.2.1" - -remove-bom-stream@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz#05f1a593f16e42e1fb90ebf59de8e569525f9523" - integrity sha1-BfGlk/FuQuH7kOv1nejlaVJflSM= - dependencies: - remove-bom-buffer "^3.0.0" - safe-buffer "^5.1.0" - through2 "^2.0.3" - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= - -replace-ext@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924" - integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ= - -replace-ext@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a" - integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw== - -request@2.81.0, request@~2.81.0: - version "2.81.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" - integrity sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA= - dependencies: - aws-sign2 "~0.6.0" - aws4 "^1.2.1" - caseless "~0.12.0" - combined-stream "~1.0.5" - extend "~3.0.0" - forever-agent "~0.6.1" - form-data "~2.1.1" - har-validator "~4.2.1" - hawk "~3.1.3" - http-signature "~1.1.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - oauth-sign "~0.8.1" - performance-now "^0.2.0" - qs "~6.4.0" - safe-buffer "^5.0.1" - stringstream "~0.0.4" - tough-cookie "~2.3.0" - tunnel-agent "^0.6.0" - uuid "^3.0.0" - -request@^2.85.0: - version "2.85.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa" - integrity sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.6.0" - caseless "~0.12.0" - combined-stream "~1.0.5" - extend "~3.0.1" - forever-agent "~0.6.1" - form-data "~2.3.1" - har-validator "~5.0.3" - hawk "~6.0.2" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.17" - oauth-sign "~0.8.2" - performance-now "^2.1.0" - qs "~6.5.1" - safe-buffer "^5.1.1" - stringstream "~0.0.5" - tough-cookie "~2.3.3" - tunnel-agent "^0.6.0" - uuid "^3.1.0" - request@^2.86.0: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" @@ -2823,73 +1350,26 @@ request@^2.86.0: tunnel-agent "^0.6.0" uuid "^3.3.2" -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -resolve-options@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/resolve-options/-/resolve-options-1.1.0.tgz#32bb9e39c06d67338dc9378c0d6d6074566ad131" - integrity sha1-MrueOcBtZzONyTeMDW1gdFZq0TE= - dependencies: - value-or-function "^3.0.0" - -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= - -reusify@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -run-parallel@^1.1.9: - version "1.1.9" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" - integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== - -safe-buffer@^5.0.1, safe-buffer@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" - integrity sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg== - -safe-buffer@^5.1.0: +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -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: +safe-buffer@~5.1.0: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -sax@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/sax/-/sax-0.5.2.tgz#735ffaa39a1cff8ffb9598f0223abdb03a9fb2ea" - integrity sha1-c1/6o5oc/4/7lZjwIjq9sDqfsuo= +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== sax@0.5.x: version "0.5.8" resolved "https://registry.yarnpkg.com/sax/-/sax-0.5.8.tgz#d472db228eb331c2506b0e8c15524adb939d12c1" integrity sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE= -sax@>=0.6.0: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - semaphore@^1.0.5: version "1.1.0" resolved "https://registry.yarnpkg.com/semaphore/-/semaphore-1.1.0.tgz#aaad8b86b20fe8e9b32b16dc2ee682a8cd26a8aa" @@ -2905,146 +1385,51 @@ semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -set-blocking@^2.0.0: +semver@^7.3.2: + version "7.3.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" + integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== + dependencies: + lru-cache "^6.0.0" + +shebang-command@^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= + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" -slash@^3.0.0: +shebang-regex@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -sntp@1.x.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" - integrity sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg= - dependencies: - hoek "2.x.x" - -sntp@2.x.x: - version "2.1.0" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" - integrity sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg== - dependencies: - hoek "4.x.x" - -source-map-resolve@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" - integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA== - dependencies: - atob "^2.1.1" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-support@~0.5.12: - version "0.5.13" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" - integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-url@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" - integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= - -source-map@^0.5.1: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: +source-map@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -sparkles@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.1.tgz#008db65edce6c50eec0c5e228e1945061dd0437c" - integrity sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw== - -split@0.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" - integrity sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8= - dependencies: - through "2" - 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= sshpk@^1.7.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" - integrity sha1-US322mKHFEMW3EwY/hzx2UBzm+M= + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== dependencies: asn1 "~0.2.3" assert-plus "^1.0.0" - dashdash "^1.12.0" - getpass "^0.1.1" - optionalDependencies: bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" ecc-jsbn "~0.1.1" + getpass "^0.1.1" jsbn "~0.1.0" + safer-buffer "^2.0.2" tweetnacl "~0.14.0" -stream-combiner@~0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" - integrity sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ= - dependencies: - duplexer "~0.1.1" - -stream-shift@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" - integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== - -stream-to-array@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/stream-to-array/-/stream-to-array-2.3.0.tgz#bbf6b39f5f43ec30bc71babcb37557acecf34353" - integrity sha1-u/azn19D7DC8cbq8s3VXrOzzQ1M= - dependencies: - any-promise "^1.1.0" - -streamifier@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/streamifier/-/streamifier-0.1.1.tgz#97e98d8fa4d105d62a2691d1dc07e820db8dfc4f" - integrity sha1-l+mNj6TRBdYqJpHR3AfoINuN/E8= - -string-width@^4.1.0, string-width@^4.2.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.prototype.trimend@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz#6ddd9a8796bc714b489a3ae22246a208f37bfa46" - integrity sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" - -string.prototype.trimstart@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz#22d45da81015309cd0cdd79787e8919fc5c613e7" - integrity sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" - -string_decoder@^1.1.1, string_decoder@~1.1.1: +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== @@ -3056,89 +1441,6 @@ string_decoder@~0.10.x: resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= -stringstream@~0.0.4, stringstream@~0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" - integrity sha1-TkhM1N5aC7vuGORjB3EKioFiGHg= - -strip-ansi@^3.0.0: - 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@^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@2.X: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= - dependencies: - is-utf8 "^0.2.0" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - -terser@*: - version "4.2.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.2.1.tgz#1052cfe17576c66e7bc70fcc7119f22b155bdac1" - integrity sha512-cGbc5utAcX4a9+2GGVX4DsenG6v0x3glnDi5hx8816X1McEAwPlPgRtXPJzSBsbpILxZ8MQMT0KvArLuE0HP5A== - dependencies: - commander "^2.20.0" - source-map "~0.6.1" - source-map-support "~0.5.12" - -terser@4.3.8: - version "4.3.8" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.3.8.tgz#707f05f3f4c1c70c840e626addfdb1c158a17136" - integrity sha512-otmIRlRVmLChAWsnSFNO0Bfk6YySuBp6G9qrHiJwlLDd4mxe2ta4sjI7TzIR+W1nBMjilzrMcPOz9pSusgx3hQ== - dependencies: - commander "^2.20.0" - source-map "~0.6.1" - source-map-support "~0.5.12" - -through2-filter@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254" - integrity sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA== - dependencies: - through2 "~2.0.0" - xtend "~4.0.0" - -through2@2.X, through2@^2.0.0, through2@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" - integrity sha1-AARWmzfHx0ujnEPzzteNGtlBQL4= - dependencies: - readable-stream "^2.1.5" - xtend "~4.0.1" - -through2@~2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" - integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== - dependencies: - readable-stream "~2.3.6" - xtend "~4.0.1" - -through@2, through@~2.3, through@~2.3.1: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= - -time-stamp@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" - integrity sha1-dkpaEa9QVhkhsTPztE5hhofg9cM= - tmp@0.0.29: version "0.0.29" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.29.tgz#f25125ff0dd9da3ccb0c2dd371ee1288bb9128c0" @@ -3146,42 +1448,6 @@ tmp@0.0.29: dependencies: os-tmpdir "~1.0.1" -to-absolute-glob@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b" - integrity sha1-GGX0PZ50sIItufFFt4z/fQ98hJs= - dependencies: - is-absolute "^1.0.0" - is-negated-glob "^1.0.0" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -to-through@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-through/-/to-through-2.0.0.tgz#fc92adaba072647bc0b67d6b03664aa195093af6" - integrity sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY= - dependencies: - through2 "^2.0.3" - -tough-cookie@~2.3.0: - version "2.3.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" - integrity sha1-C2GKVWW23qkL80JdBNVe3EdadWE= - dependencies: - punycode "^1.4.1" - -tough-cookie@~2.3.3: - version "2.3.4" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" - integrity sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA== - dependencies: - punycode "^1.4.1" - tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -3190,20 +1456,6 @@ tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" -ts-morph@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-3.1.3.tgz#bbfa1d14481ee23bdd1c030340ccf4a243cfc844" - integrity sha512-CwjgyJTtd3f8vBi7Vr0IOgdOY6Wi/Tq0MhieXOE2B5ns5WWRD7BwMNHtv+ZufKI/S2U/lMrh+Q3bOauE4tsv2g== - dependencies: - "@dsherret/to-absolute-glob" "^2.0.2" - code-block-writer "9.4.1" - fs-extra "^8.1.0" - glob-parent "^5.0.0" - globby "^10.0.1" - is-negated-glob "^1.0.0" - multimatch "^4.0.0" - typescript "^3.0.1" - tslib@^1.8.1: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" @@ -3246,39 +1498,21 @@ typed-rest-client@^0.9.0: tunnel "0.0.4" underscore "1.8.3" -typescript@^3.0.1: - version "3.5.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" - integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== +typescript@4.2.0-dev.20201207: + version "4.2.0-dev.20201207" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.0-dev.20201207.tgz#19a34bc7d2d42a7467c512c63f135587ac848807" + integrity sha512-fPHBDi/fgdX4WiRC7cFVv/aL069PgUaDWuLYUSHatWZujz/Lkc9bkf/zL3rKdNSCxlNKAMs3fhJv/yompOphZA== -typescript@^4.2.0-dev.20201119: - version "4.2.0-dev.20201119" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.0-dev.20201119.tgz#d4a43511cd9931adac05e1a47b6425f6b0e76cc3" - integrity sha512-HIgv+D/0VpVYRTbcVVf9oac/0GtLKMqaufTcPgohNaFWlCOh4lq8syefANgENXTG5Q4VEC6xwDGzHW6EJAVr3A== - -typical@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" - integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== +typescript@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7" + integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.5.tgz#0c65f15f815aa08b560a61ce8b4db7ffc3f45376" integrity sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg== -uglify-js@^3.0.5: - 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" - -unc-path-regex@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" - integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= - underscore@1.8.3, underscore@~1.8.3: version "1.8.3" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" @@ -3289,36 +1523,23 @@ underscore@^1.8.3: resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" integrity sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg== -unique-stream@^2.0.2: - version "2.3.1" - resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.3.1.tgz#c65d110e9a4adf9a6c5948b28053d9a8d04cbeac" - integrity sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A== - dependencies: - json-stable-stringify-without-jsonify "^1.0.1" - through2-filter "^3.0.0" - universal-user-agent@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== -universalify@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" - integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== uri-js@^4.2.2: - version "4.4.0" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" - integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: punycode "^2.1.0" -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= - url-join@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/url-join/-/url-join-1.1.0.tgz#741c6c2f4596c4830d6718460920d0c92202dc78" @@ -3329,17 +1550,7 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -uuid@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" - integrity sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g== - -uuid@^3.1.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" - integrity sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA== - -uuid@^3.3.2: +uuid@^3.0.0, uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -3349,21 +1560,11 @@ uuid@^8.3.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31" integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg== -validator@~3.35.0: - version "3.35.0" - resolved "https://registry.yarnpkg.com/validator/-/validator-3.35.0.tgz#3f07249402c1fc8fc093c32c6e43d72a79cca1dc" - integrity sha1-PwcklALB/I/Ak8MsbkPXKnnModw= - validator@~9.4.1: version "9.4.1" resolved "https://registry.yarnpkg.com/validator/-/validator-9.4.1.tgz#abf466d398b561cd243050112c6ff1de6cc12663" integrity sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA== -value-or-function@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-3.0.0.tgz#1c243a50b595c1be54a754bfece8563b9ff8d813" - integrity sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM= - verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" @@ -3373,79 +1574,6 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -vinyl-fs@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-3.0.3.tgz#c85849405f67428feabbbd5c5dbdd64f47d31bc7" - integrity sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng== - dependencies: - fs-mkdirp-stream "^1.0.0" - glob-stream "^6.1.0" - graceful-fs "^4.0.0" - is-valid-glob "^1.0.0" - lazystream "^1.0.0" - lead "^1.0.0" - object.assign "^4.0.4" - pumpify "^1.3.5" - readable-stream "^2.3.3" - remove-bom-buffer "^3.0.0" - remove-bom-stream "^1.2.0" - resolve-options "^1.1.0" - through2 "^2.0.0" - to-through "^2.0.0" - value-or-function "^3.0.0" - vinyl "^2.0.0" - vinyl-sourcemap "^1.1.0" - -vinyl-sourcemap@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz#92a800593a38703a8cdb11d8b300ad4be63b3e16" - integrity sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY= - dependencies: - append-buffer "^1.0.2" - convert-source-map "^1.5.0" - graceful-fs "^4.1.6" - normalize-path "^2.1.1" - now-and-later "^2.0.0" - remove-bom-buffer "^3.0.0" - vinyl "^2.0.0" - -vinyl-sourcemaps-apply@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" - integrity sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU= - dependencies: - source-map "^0.5.1" - -vinyl@1.X: - version "1.2.0" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884" - integrity sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ= - dependencies: - clone "^1.0.0" - clone-stats "^0.0.1" - replace-ext "0.0.1" - -vinyl@^0.5.0: - version "0.5.3" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.5.3.tgz#b0455b38fc5e0cf30d4325132e461970c2091cde" - integrity sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4= - dependencies: - clone "^1.0.0" - clone-stats "^0.0.1" - replace-ext "0.0.1" - -vinyl@^2.0.0, vinyl@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.1.tgz#23cfb8bbab5ece3803aa2c0a1eb28af7cbba1974" - integrity sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw== - dependencies: - clone "^2.1.1" - clone-buffer "^1.0.0" - clone-stats "^1.0.0" - cloneable-readable "^1.0.0" - remove-trailing-separator "^1.0.1" - replace-ext "^1.0.0" - vsce@1.48.0: version "1.48.0" resolved "https://registry.yarnpkg.com/vsce/-/vsce-1.48.0.tgz#31c1a4c6909c3b8bdc48b3d32cc8c8e94c7113a2" @@ -3469,22 +1597,19 @@ vsce@1.48.0: yauzl "^2.3.1" yazl "^2.2.2" -vscode-ripgrep@^1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/vscode-ripgrep/-/vscode-ripgrep-1.6.2.tgz#fb912c7465699f10ce0218a6676cc632c77369b4" - integrity sha512-jkZEWnQFcE+QuQFfxQXWcWtDafTmgkp3DjMKawDkajZwgnDlGKpFp15ybKrZNVTi1SLEF/12BzxYSZVVZ2XrkA== +vscode-universal@deepak1556/universal#61454d96223b774c53cda10f72c2098c0ce02d58: + version "0.0.2" + resolved "https://codeload.github.com/deepak1556/universal/tar.gz/61454d96223b774c53cda10f72c2098c0ce02d58" dependencies: - https-proxy-agent "^4.0.0" - proxy-from-env "^1.1.0" - -vscode-telemetry-extractor@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/vscode-telemetry-extractor/-/vscode-telemetry-extractor-1.6.0.tgz#e9d9c1d24863cce8d3d715f0287de3b31eb90c56" - integrity sha512-zSxvkbyAMa1lTRGIHfGg7gW2e9Sey+2zGYD19uNWCsVEfoXAr2NB6uzb0sNHtbZ2SSqxSePmFXzBAavsudT5fw== - dependencies: - command-line-args "^5.1.1" - ts-morph "^3.1.3" - vscode-ripgrep "^1.6.2" + "@malept/cross-spawn-promise" "^1.1.0" + "@types/debug" "^4.1.5" + "@types/fs-extra" "^9.0.6" + "@types/node" "^14.14.21" + asar "^3.0.3" + debug "^4.3.1" + dir-compare "^2.4.0" + fs-extra "^9.0.1" + typescript "^4.1.3" vso-node-api@6.1.2-preview: version "6.1.2-preview" @@ -3496,37 +1621,18 @@ vso-node-api@6.1.2-preview: typed-rest-client "^0.9.0" underscore "^1.8.3" -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" - integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= - -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" + isexe "^2.0.0" wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -xml2js@0.2.7: - version "0.2.7" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.2.7.tgz#1838518bb01741cae0878bab4915e494c32306af" - integrity sha1-GDhRi7AXQcrgh4urSRXklMMjBq8= - dependencies: - sax "0.5.2" - xml2js@0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.2.8.tgz#9b81690931631ff09d1957549faf54f4f980b3c2" @@ -3534,73 +1640,25 @@ xml2js@0.2.8: dependencies: sax "0.5.x" -xml2js@^0.4.17: - version "0.4.19" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" - integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== - dependencies: - sax ">=0.6.0" - xmlbuilder "~9.0.1" - -xmlbuilder@0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-0.4.3.tgz#c4614ba74e0ad196e609c9272cd9e1ddb28a8a58" - integrity sha1-xGFLp04K0ZbmCcknLNnh3bKKilg= +xmlbuilder@>=11.0.1: + version "15.1.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" + integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== xmlbuilder@^9.0.7: version "9.0.7" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= -xmlbuilder@~9.0.1: - version "9.0.4" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.4.tgz#519cb4ca686d005a8420d3496f3f0caeecca580f" - integrity sha1-UZy0ymhtAFqEINNJbz8MruzKWA8= - xmldom@0.1.x: version "0.1.31" resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff" integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ== -xtend@~4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -xtend@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" - integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= - -y18n@^4.0.0: +yallist@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== - -yargs-parser@^18.1.2: - version "18.1.3" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs@^15.3.0: - version "15.4.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" - integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== - dependencies: - cliui "^6.0.0" - decamelize "^1.2.0" - find-up "^4.1.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^4.2.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^18.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yauzl@^2.3.1: version "2.10.0" diff --git a/cgmanifest.json b/cgmanifest.json index 656e3f82..a47ba0d6 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "894fb9eb56c6cbda65e3c3ae9ada6d4cb5850cc9" + "commitHash": "c0dfcf99c0bbc9c4763c70e5034eb1a970a9ff3b" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "83.0.4103.122" + "version": "87.0.4280.141" }, { "component": { @@ -48,11 +48,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "9622fed3fb2cffcea9efff6c8cb4cc2def99d75d" + "commitHash": "e3e0927bb93ed92bcdfe81e7ad9af3d78ccc74fb" } }, "isOnlyProductionDependency": true, - "version": "12.14.1" + "version": "12.18.3" }, { "component": { @@ -60,12 +60,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "415c1f9e9b35d9599b1a8ad1200476afa47a3323" + "commitHash": "8805b996e0d8cfb6e3921f9b586366bafb125b59" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "9.3.5" + "version": "11.2.1" }, { "component": { @@ -132,11 +132,11 @@ "git": { "name": "vscode-codicons", "repositoryUrl": "https://github.com/microsoft/vscode-codicons", - "commitHash": "f0caa623812a8ed5059516277675b4158d4c4867" + "commitHash": "ccdcf91d57d3a5a1d6b620d95d518bab4d75984d" } }, "license": "MIT and Creative Commons Attribution 4.0", - "version": "0.0.1" + "version": "0.0.14" }, { "component": { diff --git a/extensions/clojure/language-configuration.json b/extensions/clojure/language-configuration.json index a62cb968..f6ab4a26 100644 --- a/extensions/clojure/language-configuration.json +++ b/extensions/clojure/language-configuration.json @@ -1,6 +1,6 @@ { "comments": { - "lineComment": ";" + "lineComment": ";;" }, "brackets": [ ["{", "}"], diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index 42340041..26348576 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -128,6 +128,6 @@ ] }, "devDependencies": { - "@types/node": "^12.11.7" + "@types/node": "^12.19.9" } } diff --git a/extensions/configuration-editing/src/configurationEditingMain.ts b/extensions/configuration-editing/src/configurationEditingMain.ts index 966073e2..a3ef34f3 100644 --- a/extensions/configuration-editing/src/configurationEditingMain.ts +++ b/extensions/configuration-editing/src/configurationEditingMain.ts @@ -81,7 +81,7 @@ function registerExtensionsCompletionsInExtensionsDocument(): vscode.Disposable const range = document.getWordRangeAtPosition(position) || new vscode.Range(position, position); if (location.path[0] === 'recommendations') { const extensionsContent = parse(document.getText()); - return provideInstalledExtensionProposals(extensionsContent && extensionsContent.recommendations || [], range, false); + return provideInstalledExtensionProposals(extensionsContent && extensionsContent.recommendations || [], '', range, false); } return []; } @@ -95,7 +95,7 @@ function registerExtensionsCompletionsInWorkspaceConfigurationDocument(): vscode const range = document.getWordRangeAtPosition(position) || new vscode.Range(position, position); if (location.path[0] === 'extensions' && location.path[1] === 'recommendations') { const extensionsContent = parse(document.getText())['extensions']; - return provideInstalledExtensionProposals(extensionsContent && extensionsContent.recommendations || [], range, false); + return provideInstalledExtensionProposals(extensionsContent && extensionsContent.recommendations || [], '', range, false); } return []; } diff --git a/extensions/configuration-editing/src/extensionsProposals.ts b/extensions/configuration-editing/src/extensionsProposals.ts index 60533ae2..7fef9836 100644 --- a/extensions/configuration-editing/src/extensionsProposals.ts +++ b/extensions/configuration-editing/src/extensionsProposals.ts @@ -8,14 +8,14 @@ import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); -export function provideInstalledExtensionProposals(existing: string[], range: vscode.Range, includeBuiltinExtensions: boolean): vscode.ProviderResult { +export function provideInstalledExtensionProposals(existing: string[], additionalText: string, range: vscode.Range, includeBuiltinExtensions: boolean): vscode.ProviderResult { if (Array.isArray(existing)) { const extensions = includeBuiltinExtensions ? vscode.extensions.all : vscode.extensions.all.filter(e => !(e.id.startsWith('vscode.') || e.id === 'Microsoft.vscode-markdown')); const knownExtensionProposals = extensions.filter(e => existing.indexOf(e.id) === -1); if (knownExtensionProposals.length) { return knownExtensionProposals.map(e => { const item = new vscode.CompletionItem(e.id); - const insertText = `"${e.id}"`; + const insertText = `"${e.id}"${additionalText}`; item.kind = vscode.CompletionItemKind.Value; item.insertText = insertText; item.range = range; diff --git a/extensions/configuration-editing/src/settingsDocumentHelper.ts b/extensions/configuration-editing/src/settingsDocumentHelper.ts index 5e466c2e..f3461f0c 100644 --- a/extensions/configuration-editing/src/settingsDocumentHelper.ts +++ b/extensions/configuration-editing/src/settingsDocumentHelper.ts @@ -48,7 +48,16 @@ export class SettingsDocument { try { ignoredExtensions = parse(this.document.getText())['settingsSync.ignoredExtensions']; } catch (e) {/* ignore error */ } - return provideInstalledExtensionProposals(ignoredExtensions, range, true); + return provideInstalledExtensionProposals(ignoredExtensions, '', range, true); + } + + // remote.extensionKind + if (location.path[0] === 'remote.extensionKind' && location.path.length === 2 && location.isAtPropertyKey) { + let alreadyConfigured: string[] = []; + try { + alreadyConfigured = Object.keys(parse(this.document.getText())['remote.extensionKind']); + } catch (e) {/* ignore error */ } + return provideInstalledExtensionProposals(alreadyConfigured, `: [\n\t"ui"\n]`, range, true); } return this.provideLanguageOverridesCompletionItems(location, position); diff --git a/extensions/configuration-editing/yarn.lock b/extensions/configuration-editing/yarn.lock index 36aab5fd..ebfaa046 100644 --- a/extensions/configuration-editing/yarn.lock +++ b/extensions/configuration-editing/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@^12.11.7": - version "12.11.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.11.7.tgz#57682a9771a3f7b09c2497f28129a0462966524a" - integrity sha512-JNbGaHFCLwgHn/iCckiGSOZ1XYHsKFwREtzPwSGCVld1SGhOlmZw2D4ZI94HQCrBHbADzW9m4LER/8olJTRGHA== +"@types/node@^12.19.9": + version "12.19.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" + integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== jsonc-parser@^2.2.1: version "2.2.1" diff --git a/extensions/cpp/cgmanifest.json b/extensions/cpp/cgmanifest.json index 05938de6..a483664d 100644 --- a/extensions/cpp/cgmanifest.json +++ b/extensions/cpp/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "jeff-hykin/cpp-textmate-grammar", "repositoryUrl": "https://github.com/jeff-hykin/cpp-textmate-grammar", - "commitHash": "ad9f1541fd376740c30a7257614c9cb5ed25005d" + "commitHash": "f074a48ae0b7ba313af3faf3d8bfda8537864bd1" } }, "license": "MIT", - "version": "1.15.3", + "version": "1.15.5", "description": "The files syntaxes/c.json and syntaxes/c++.json were derived from https://github.com/atom/language-c which was originally converted from the C TextMate bundle https://github.com/textmate/c.tmbundle." }, { diff --git a/extensions/cpp/syntaxes/c.tmLanguage.json b/extensions/cpp/syntaxes/c.tmLanguage.json index 952730f5..7ce64b81 100644 --- a/extensions/cpp/syntaxes/c.tmLanguage.json +++ b/extensions/cpp/syntaxes/c.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/jeff-hykin/cpp-textmate-grammar/commit/afa42b3f5529833714ae354fbc638ece8f253074", + "version": "https://github.com/jeff-hykin/cpp-textmate-grammar/commit/f074a48ae0b7ba313af3faf3d8bfda8537864bd1", "name": "C", "scopeName": "source.c", "patterns": [ @@ -2939,9 +2939,6 @@ } } }, - { - "include": "#comments_context" - }, { "include": "#comments" }, @@ -3104,9 +3101,6 @@ "match": ":", "name": "punctuation.separator.delimiter.colon.assembly.c" }, - { - "include": "#comments_context" - }, { "include": "#comments" } diff --git a/extensions/cpp/syntaxes/cpp.embedded.macro.tmLanguage.json b/extensions/cpp/syntaxes/cpp.embedded.macro.tmLanguage.json index 151cad6f..23f35fd5 100644 --- a/extensions/cpp/syntaxes/cpp.embedded.macro.tmLanguage.json +++ b/extensions/cpp/syntaxes/cpp.embedded.macro.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/jeff-hykin/cpp-textmate-grammar/commit/ad9f1541fd376740c30a7257614c9cb5ed25005d", + "version": "https://github.com/jeff-hykin/cpp-textmate-grammar/commit/f074a48ae0b7ba313af3faf3d8bfda8537864bd1", "name": "C++", "scopeName": "source.cpp.embedded.macro", "patterns": [ @@ -298,9 +298,6 @@ } } }, - { - "include": "#comments_context" - }, { "include": "#comments" }, @@ -463,9 +460,6 @@ "match": ":", "name": "punctuation.separator.delimiter.colon.assembly.cpp" }, - { - "include": "#comments_context" - }, { "include": "#comments" } diff --git a/extensions/cpp/syntaxes/cpp.tmLanguage.json b/extensions/cpp/syntaxes/cpp.tmLanguage.json index 43ce9526..ecd7d3db 100644 --- a/extensions/cpp/syntaxes/cpp.tmLanguage.json +++ b/extensions/cpp/syntaxes/cpp.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/jeff-hykin/cpp-textmate-grammar/commit/dc9c3ed759c84e0b168cc2140e6869ca97abbd14", + "version": "https://github.com/jeff-hykin/cpp-textmate-grammar/commit/f074a48ae0b7ba313af3faf3d8bfda8537864bd1", "name": "C++", "scopeName": "source.cpp", "patterns": [ @@ -344,9 +344,6 @@ } } }, - { - "include": "#comments_context" - }, { "include": "#comments" }, @@ -509,9 +506,6 @@ "match": ":", "name": "punctuation.separator.delimiter.colon.assembly.cpp" }, - { - "include": "#comments_context" - }, { "include": "#comments" } diff --git a/extensions/css-language-features/client/src/customData.ts b/extensions/css-language-features/client/src/customData.ts index 24b56f12..bab5cc40 100644 --- a/extensions/css-language-features/client/src/customData.ts +++ b/extensions/css-language-features/client/src/customData.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { workspace, extensions, Uri, EventEmitter, Disposable } from 'vscode'; -import { resolvePath, joinPath } from './requests'; +import { Utils } from 'vscode-uri'; export function getCustomDataSource(toDispose: Disposable[]) { let pathsInWorkspace = getCustomDataPathsInAllWorkspaces(); @@ -50,7 +50,7 @@ function getCustomDataPathsInAllWorkspaces(): string[] { if (Array.isArray(paths)) { for (const path of paths) { if (typeof path === 'string') { - dataPaths.push(resolvePath(rootFolder, path).toString()); + dataPaths.push(Utils.resolvePath(rootFolder, path).toString()); } } } @@ -80,7 +80,7 @@ function getCustomDataPathsFromAllExtensions(): string[] { const customData = extension.packageJSON?.contributes?.css?.customData; if (Array.isArray(customData)) { for (const rp of customData) { - dataPaths.push(joinPath(extension.extensionUri, rp).toString()); + dataPaths.push(Utils.joinPath(extension.extensionUri, rp).toString()); } } } diff --git a/extensions/css-language-features/client/src/node/nodeFs.ts b/extensions/css-language-features/client/src/node/nodeFs.ts index c13ef2e1..0b572018 100644 --- a/extensions/css-language-features/client/src/node/nodeFs.ts +++ b/extensions/css-language-features/client/src/node/nodeFs.ts @@ -5,11 +5,11 @@ import * as fs from 'fs'; import { Uri } from 'vscode'; -import { getScheme, RequestService, FileType } from '../requests'; +import { RequestService, FileType } from '../requests'; export function getNodeFSRequestService(): RequestService { function ensureFileUri(location: string) { - if (getScheme(location) !== 'file') { + if (!location.startsWith('file://')) { throw new Error('fileRequestService can only handle file URLs'); } } diff --git a/extensions/css-language-features/client/src/requests.ts b/extensions/css-language-features/client/src/requests.ts index 1b1e70b2..f06ce978 100644 --- a/extensions/css-language-features/client/src/requests.ts +++ b/extensions/css-language-features/client/src/requests.ts @@ -8,14 +8,14 @@ import { RequestType, CommonLanguageClient } from 'vscode-languageclient'; import { Runtime } from './cssClient'; export namespace FsContentRequest { - export const type: RequestType<{ uri: string; encoding?: string; }, string, any, any> = new RequestType('fs/content'); + export const type: RequestType<{ uri: string; encoding?: string; }, string, any> = new RequestType('fs/content'); } export namespace FsStatRequest { - export const type: RequestType = new RequestType('fs/stat'); + export const type: RequestType = new RequestType('fs/stat'); } export namespace FsReadDirRequest { - export const type: RequestType = new RequestType('fs/readDir'); + export const type: RequestType = new RequestType('fs/readDir'); } export function serveFileSystemRequests(client: CommonLanguageClient, runtime: Runtime) { @@ -88,61 +88,3 @@ export interface RequestService { stat(uri: string): Promise; readDirectory(uri: string): Promise<[string, FileType][]>; } - -export function getScheme(uri: string) { - return uri.substr(0, uri.indexOf(':')); -} - -export function dirname(uri: string) { - const lastIndexOfSlash = uri.lastIndexOf('/'); - return lastIndexOfSlash !== -1 ? uri.substr(0, lastIndexOfSlash) : ''; -} - -export function basename(uri: string) { - const lastIndexOfSlash = uri.lastIndexOf('/'); - return uri.substr(lastIndexOfSlash + 1); -} - -const Slash = '/'.charCodeAt(0); -const Dot = '.'.charCodeAt(0); - -export function isAbsolutePath(path: string) { - return path.charCodeAt(0) === Slash; -} - -export function resolvePath(uri: Uri, path: string): Uri { - if (isAbsolutePath(path)) { - return uri.with({ path: normalizePath(path.split('/')) }); - } - return joinPath(uri, path); -} - -export function normalizePath(parts: string[]): string { - const newParts: string[] = []; - for (const part of parts) { - if (part.length === 0 || part.length === 1 && part.charCodeAt(0) === Dot) { - // ignore - } else if (part.length === 2 && part.charCodeAt(0) === Dot && part.charCodeAt(1) === Dot) { - newParts.pop(); - } else { - newParts.push(part); - } - } - if (parts.length > 1 && parts[parts.length - 1].length === 0) { - newParts.push(''); - } - let res = newParts.join('/'); - if (parts[0].length === 0) { - res = '/' + res; - } - return res; -} - - -export function joinPath(uri: Uri, ...paths: string[]): Uri { - const parts = uri.path.split('/'); - for (let path of paths) { - parts.push(...path.split('/')); - } - return uri.with({ path: normalizePath(parts) }); -} diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index 29f056e4..7450976d 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -21,8 +21,7 @@ "scripts": { "compile": "gulp compile-extension:css-language-features-client compile-extension:css-language-features-server", "watch": "gulp watch-extension:css-language-features-client watch-extension:css-language-features-server", - "test": "mocha", - "postinstall": "cd server && yarn install", + "test": "node ../../node_modules/mocha/bin/mocha", "install-client-next": "yarn add vscode-languageclient@next" }, "categories": [ @@ -807,11 +806,11 @@ ] }, "dependencies": { - "vscode-languageclient": "7.0.0-next.5.1", - "vscode-nls": "^4.1.2" + "vscode-languageclient": "^7.0.0", + "vscode-nls": "^5.0.0", + "vscode-uri": "^3.0.2" }, "devDependencies": { - "@types/node": "^12.11.7", - "mocha": "^7.0.1" + "@types/node": "^12.19.9" } } diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index 5a2cd56c..e271bfd7 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -10,25 +10,21 @@ "main": "./out/node/cssServerMain", "browser": "./dist/browser/cssServerMain", "dependencies": { - "vscode-css-languageservice": "^4.4.0", - "vscode-languageserver": "7.0.0-next.3", - "vscode-uri": "^2.1.2" + "vscode-css-languageservice": "^5.0.3", + "vscode-languageserver": "^7.0.0", + "vscode-uri": "^3.0.2" }, "devDependencies": { - "@types/mocha": "7.0.2", - "@types/node": "^12.11.7", - "glob": "^7.1.6", - "mocha": "^7.1.2", - "mocha-junit-reporter": "^1.23.3", - "mocha-multi-reporters": "^1.1.7" + "@types/mocha": "^8.2.0", + "@types/node": "^12.19.9" }, "scripts": { "compile": "gulp compile-extension:css-language-features-server", "watch": "gulp watch-extension:css-language-features-server", "install-service-next": "yarn add vscode-css-languageservice@next", - "install-service-local": "npm install ../../../../vscode-css-languageservice -f", + "install-service-local": "yarn link vscode-css-languageservice", "install-server-next": "yarn add vscode-languageserver@next", - "install-server-local": "npm install ../../../../vscode-languageserver-node/server -f", + "install-server-local": "yarn link vscode-languageserver", "test": "node ./test/index.js" } } diff --git a/extensions/css-language-features/server/src/node/nodeFs.ts b/extensions/css-language-features/server/src/node/nodeFs.ts index c7b13012..c72617e3 100644 --- a/extensions/css-language-features/server/src/node/nodeFs.ts +++ b/extensions/css-language-features/server/src/node/nodeFs.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { RequestService, getScheme } from '../requests'; +import { RequestService } from '../requests'; import { URI as Uri } from 'vscode-uri'; import * as fs from 'fs'; @@ -11,7 +11,7 @@ import { FileType } from 'vscode-css-languageservice'; export function getNodeFSRequestService(): RequestService { function ensureFileUri(location: string) { - if (getScheme(location) !== 'file') { + if (!location.startsWith('file://')) { throw new Error('fileRequestService can only handle file URLs'); } } diff --git a/extensions/css-language-features/server/src/requests.ts b/extensions/css-language-features/server/src/requests.ts index 79f166c2..8ace0b0c 100644 --- a/extensions/css-language-features/server/src/requests.ts +++ b/extensions/css-language-features/server/src/requests.ts @@ -3,19 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vscode-uri'; import { RequestType, Connection } from 'vscode-languageserver'; import { RuntimeEnvironment } from './cssServer'; export namespace FsContentRequest { - export const type: RequestType<{ uri: string; encoding?: string; }, string, any, any> = new RequestType('fs/content'); + export const type: RequestType<{ uri: string; encoding?: string; }, string, any> = new RequestType('fs/content'); } export namespace FsStatRequest { - export const type: RequestType = new RequestType('fs/stat'); + export const type: RequestType = new RequestType('fs/stat'); } export namespace FsReadDirRequest { - export const type: RequestType = new RequestType('fs/readDir'); + export const type: RequestType = new RequestType('fs/readDir'); } export enum FileType { @@ -99,79 +98,6 @@ export function getRequestService(handledSchemas: string[], connection: Connecti }; } -export function getScheme(uri: string) { +function getScheme(uri: string) { return uri.substr(0, uri.indexOf(':')); } - -export function dirname(uri: string) { - const lastIndexOfSlash = uri.lastIndexOf('/'); - return lastIndexOfSlash !== -1 ? uri.substr(0, lastIndexOfSlash) : ''; -} - -export function basename(uri: string) { - const lastIndexOfSlash = uri.lastIndexOf('/'); - return uri.substr(lastIndexOfSlash + 1); -} - - -const Slash = '/'.charCodeAt(0); -const Dot = '.'.charCodeAt(0); - -export function extname(uri: string) { - for (let i = uri.length - 1; i >= 0; i--) { - const ch = uri.charCodeAt(i); - if (ch === Dot) { - if (i > 0 && uri.charCodeAt(i - 1) !== Slash) { - return uri.substr(i); - } else { - break; - } - } else if (ch === Slash) { - break; - } - } - return ''; -} - -export function isAbsolutePath(path: string) { - return path.charCodeAt(0) === Slash; -} - -export function resolvePath(uriString: string, path: string): string { - if (isAbsolutePath(path)) { - const uri = URI.parse(uriString); - const parts = path.split('/'); - return uri.with({ path: normalizePath(parts) }).toString(); - } - return joinPath(uriString, path); -} - -export function normalizePath(parts: string[]): string { - const newParts: string[] = []; - for (const part of parts) { - if (part.length === 0 || part.length === 1 && part.charCodeAt(0) === Dot) { - // ignore - } else if (part.length === 2 && part.charCodeAt(0) === Dot && part.charCodeAt(1) === Dot) { - newParts.pop(); - } else { - newParts.push(part); - } - } - if (parts.length > 1 && parts[parts.length - 1].length === 0) { - newParts.push(''); - } - let res = newParts.join('/'); - if (parts[0].length === 0) { - res = '/' + res; - } - return res; -} - -export function joinPath(uriString: string, ...paths: string[]): string { - const uri = URI.parse(uriString); - const parts = uri.path.split('/'); - for (let path of paths) { - parts.push(...path.split('/')); - } - return uri.with({ path: normalizePath(parts) }).toString(); -} diff --git a/extensions/css-language-features/server/src/test/requests.test.ts b/extensions/css-language-features/server/src/test/requests.test.ts deleted file mode 100644 index 3f287fef..00000000 --- a/extensions/css-language-features/server/src/test/requests.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import 'mocha'; -import * as assert from 'assert'; -import { joinPath, normalizePath, resolvePath, extname } from '../requests'; - -suite('requests', () => { - test('join', async function () { - assert.equal(joinPath('foo://a/foo/bar', 'x'), 'foo://a/foo/bar/x'); - assert.equal(joinPath('foo://a/foo/bar/', 'x'), 'foo://a/foo/bar/x'); - assert.equal(joinPath('foo://a/foo/bar/', '/x'), 'foo://a/foo/bar/x'); - assert.equal(joinPath('foo://a/foo/bar/', 'x/'), 'foo://a/foo/bar/x/'); - assert.equal(joinPath('foo://a/foo/bar/', 'x', 'y'), 'foo://a/foo/bar/x/y'); - assert.equal(joinPath('foo://a/foo/bar/', 'x/', '/y'), 'foo://a/foo/bar/x/y'); - assert.equal(joinPath('foo://a/foo/bar/', '.', '/y'), 'foo://a/foo/bar/y'); - assert.equal(joinPath('foo://a/foo/bar/', 'x/y/z', '..'), 'foo://a/foo/bar/x/y'); - }); - - test('resolve', async function () { - assert.equal(resolvePath('foo://a/foo/bar', 'x'), 'foo://a/foo/bar/x'); - assert.equal(resolvePath('foo://a/foo/bar/', 'x'), 'foo://a/foo/bar/x'); - assert.equal(resolvePath('foo://a/foo/bar/', '/x'), 'foo://a/x'); - assert.equal(resolvePath('foo://a/foo/bar/', 'x/'), 'foo://a/foo/bar/x/'); - }); - - test('normalize', async function () { - function assertNormalize(path: string, expected: string) { - assert.equal(normalizePath(path.split('/')), expected, path); - } - assertNormalize('a', 'a'); - assertNormalize('/a', '/a'); - assertNormalize('a/', 'a/'); - assertNormalize('a/b', 'a/b'); - assertNormalize('/a/foo/bar/x', '/a/foo/bar/x'); - assertNormalize('/a/foo/bar//x', '/a/foo/bar/x'); - assertNormalize('/a/foo/bar///x', '/a/foo/bar/x'); - assertNormalize('/a/foo/bar/x/', '/a/foo/bar/x/'); - assertNormalize('a/foo/bar/x/', 'a/foo/bar/x/'); - assertNormalize('a/foo/bar/x//', 'a/foo/bar/x/'); - assertNormalize('//a/foo/bar/x//', '/a/foo/bar/x/'); - assertNormalize('a/.', 'a'); - assertNormalize('a/./b', 'a/b'); - assertNormalize('a/././b', 'a/b'); - assertNormalize('a/n/../b', 'a/b'); - assertNormalize('a/n/../', 'a/'); - assertNormalize('a/n/../', 'a/'); - assertNormalize('/a/n/../..', '/'); - assertNormalize('..', ''); - assertNormalize('/..', '/'); - }); - - test('extname', async function () { - function assertExtName(input: string, expected: string) { - assert.equal(extname(input), expected, input); - } - assertExtName('foo://a/foo/bar', ''); - assertExtName('foo://a/foo/bar.foo', '.foo'); - assertExtName('foo://a/foo/.foo', ''); - }); -}); diff --git a/extensions/css-language-features/server/src/utils/documentContext.ts b/extensions/css-language-features/server/src/utils/documentContext.ts index f8bc67f8..a7beffd0 100644 --- a/extensions/css-language-features/server/src/utils/documentContext.ts +++ b/extensions/css-language-features/server/src/utils/documentContext.ts @@ -6,7 +6,7 @@ import { DocumentContext } from 'vscode-css-languageservice'; import { endsWith, startsWith } from '../utils/strings'; import { WorkspaceFolder } from 'vscode-languageserver'; -import { resolvePath } from '../requests'; +import { Utils, URI } from 'vscode-uri'; export function getDocumentContext(documentUri: string, workspaceFolders: WorkspaceFolder[]): DocumentContext { function getRootFolder(): string | undefined { @@ -31,7 +31,7 @@ export function getDocumentContext(documentUri: string, workspaceFolders: Worksp } } base = base.substr(0, base.lastIndexOf('/') + 1); - return resolvePath(base, ref); + return Utils.resolvePath(URI.parse(base), ref).toString(); }, }; } diff --git a/extensions/css-language-features/server/src/utils/runner.ts b/extensions/css-language-features/server/src/utils/runner.ts index ad1d779e..9b82baf5 100644 --- a/extensions/css-language-features/server/src/utils/runner.ts +++ b/extensions/css-language-features/server/src/utils/runner.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ResponseError, ErrorCodes, CancellationToken } from 'vscode-languageserver'; +import { ResponseError, CancellationToken, LSPErrorCodes } from 'vscode-languageserver'; export function formatError(message: string, err: any): string { if (err instanceof Error) { @@ -39,5 +39,5 @@ export function runSafeAsync(func: () => Thenable, errorVal: T, errorMessa } function cancelValue() { - return new ResponseError(ErrorCodes.RequestCancelled, 'Request cancelled'); + return new ResponseError(LSPErrorCodes.RequestCancelled, 'Request cancelled'); } diff --git a/extensions/css-language-features/server/yarn.lock b/extensions/css-language-features/server/yarn.lock index af0eddbb..62acbd46 100644 --- a/extensions/css-language-features/server/yarn.lock +++ b/extensions/css-language-features/server/yarn.lock @@ -2,838 +2,62 @@ # yarn lockfile v1 -"@types/mocha@7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-7.0.2.tgz#b17f16cf933597e10d6d78eae3251e692ce8b0ce" - integrity sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w== - -"@types/node@^12.11.7": - version "12.11.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.11.7.tgz#57682a9771a3f7b09c2497f28129a0462966524a" - integrity sha512-JNbGaHFCLwgHn/iCckiGSOZ1XYHsKFwREtzPwSGCVld1SGhOlmZw2D4ZI94HQCrBHbADzW9m4LER/8olJTRGHA== - -ansi-colors@3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" - integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw== - -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-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== - -ansi-styles@^3.2.0, 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" - -anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -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= - -binary-extensions@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" - integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== - -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" - -braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -browser-stdout@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" - integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== - -camelcase@^5.0.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -chalk@^2.4.2: - 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" - -charenc@~0.0.1: - version "0.0.2" - resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" - integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= - -chokidar@3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.0.tgz#12c0714668c55800f659e262d4962a97faf554a6" - integrity sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.2.0" - optionalDependencies: - fsevents "~2.1.1" - -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== - dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.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= - -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= - -crypt@~0.0.1: - version "0.0.2" - resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" - integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= - -debug@3.2.6: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== - dependencies: - ms "^2.1.1" - -debug@^2.2.0: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - -decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -define-properties@^1.1.2: - 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== - dependencies: - object-keys "^1.0.12" - -diff@3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== - -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - -es-abstract@^1.5.1: - version "1.13.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" - integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== - dependencies: - es-to-primitive "^1.2.0" - function-bind "^1.1.1" - has "^1.0.3" - is-callable "^1.1.4" - is-regex "^1.0.4" - object-keys "^1.0.12" - -es-to-primitive@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" - integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -escape-string-regexp@1.0.5, 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== - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -find-up@3.0.0, find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -flat@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.0.tgz#090bec8b05e39cba309747f1d588f04dbaf98db2" - integrity sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw== - dependencies: - is-buffer "~2.0.3" - -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= - -fsevents@~2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805" - integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -get-caller-file@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -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== - dependencies: - is-glob "^4.0.1" - -glob@7.1.3: - version "7.1.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" - integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== - 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" - -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" - -growl@1.10.5: - version "1.10.5" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" - integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== - -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-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" - integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= - -has@^1.0.1, has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -he@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -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: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-buffer@~1.1.1: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-buffer@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" - integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw== - -is-callable@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" - integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== - -is-date-object@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" - integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -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= - -is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-regex@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" - integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= - dependencies: - has "^1.0.1" - -is-symbol@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" - integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== - dependencies: - has-symbols "^1.0.0" - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -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" - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -lodash@^4.16.4, lodash@^4.17.15: - version "4.17.19" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" - integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== - -log-symbols@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" - integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== - dependencies: - chalk "^2.4.2" - -md5@^2.1.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" - integrity sha1-U6s41f48iJG6RlMp6iP6wFQBJvk= - dependencies: - charenc "~0.0.1" - crypt "~0.0.1" - is-buffer "~1.1.1" - -minimatch@3.0.4, 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.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -mkdirp@0.5.5: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - -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" - -mocha-junit-reporter@^1.23.3: - version "1.23.3" - resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-1.23.3.tgz#941e219dd759ed732f8641e165918aa8b167c981" - integrity sha512-ed8LqbRj1RxZfjt/oC9t12sfrWsjZ3gNnbhV1nuj9R/Jb5/P3Xb4duv2eCfCDMYH+fEu0mqca7m4wsiVjsxsvA== - dependencies: - debug "^2.2.0" - md5 "^2.1.0" - mkdirp "~0.5.1" - strip-ansi "^4.0.0" - xml "^1.0.0" - -mocha-multi-reporters@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/mocha-multi-reporters/-/mocha-multi-reporters-1.1.7.tgz#cc7f3f4d32f478520941d852abb64d9988587d82" - integrity sha1-zH8/TTL0eFIJQdhSq7ZNmYhYfYI= - dependencies: - debug "^3.1.0" - lodash "^4.16.4" - -mocha@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-7.1.2.tgz#8e40d198acf91a52ace122cd7599c9ab857b29e6" - integrity sha512-o96kdRKMKI3E8U0bjnfqW4QMk12MwZ4mhdBTf+B5a1q9+aq2HRnj+3ZdJu0B/ZhJeK78MgYuv6L8d/rA5AeBJA== - dependencies: - ansi-colors "3.2.3" - browser-stdout "1.3.1" - chokidar "3.3.0" - debug "3.2.6" - diff "3.5.0" - escape-string-regexp "1.0.5" - find-up "3.0.0" - glob "7.1.3" - growl "1.10.5" - he "1.2.0" - js-yaml "3.13.1" - log-symbols "3.0.0" - minimatch "3.0.4" - mkdirp "0.5.5" - ms "2.1.1" - node-environment-flags "1.0.6" - object.assign "4.1.0" - strip-json-comments "2.0.1" - supports-color "6.0.0" - which "1.3.1" - wide-align "1.1.3" - yargs "13.3.2" - yargs-parser "13.1.2" - yargs-unparser "1.6.0" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - -ms@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -node-environment-flags@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.6.tgz#a30ac13621f6f7d674260a54dede048c3982c088" - integrity sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw== - dependencies: - object.getownpropertydescriptors "^2.0.3" - semver "^5.7.0" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -object-keys@^1.0.11, object-keys@^1.0.12: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" - integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== - dependencies: - define-properties "^1.1.2" - function-bind "^1.1.1" - has-symbols "^1.0.0" - object-keys "^1.0.11" - -object.getownpropertydescriptors@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" - integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY= - dependencies: - define-properties "^1.1.2" - es-abstract "^1.5.1" - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -p-limit@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2" - integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ== - dependencies: - p-try "^2.0.0" - -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - -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= - -picomatch@^2.0.4: - version "2.2.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a" - integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA== - -readdirp@~3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.2.0.tgz#c30c33352b12c96dfb4b895421a49fd5a9593839" - integrity sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ== - dependencies: - picomatch "^2.0.4" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -semver@^5.7.0: - version "5.7.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" - integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== - -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= - -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.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-width@^3.0.0, string-width@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.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-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.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@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.0.0.tgz#76cfe742cf1f41bb9b1c29ad03068c05b4c0e40a" - integrity sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg== - dependencies: - has-flag "^3.0.0" - -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" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -vscode-css-languageservice@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.4.0.tgz#a7c5edf3057e707601ca18fa3728784a298513b4" - integrity sha512-jWi+297PJUUWTHwlcrZz0zIuEXuHOBJIQMapXmEzbosWGv/gMnNSAMV4hTKnl5wzxvZKZzV6j+WFdrSlKQ5qnw== +"@types/mocha@^8.2.0": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.0.tgz#3eb56d13a1de1d347ecb1957c6860c911704bc44" + integrity sha512-/Sge3BymXo4lKc31C8OINJgXLaw+7vL1/L1pGiBNpGrBiT8FQiaFpSYV0uhTaG4y78vcMBTMFsWaHDvuD+xGzQ== + +"@types/node@^12.19.9": + version "12.19.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" + integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== + +vscode-css-languageservice@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-5.0.3.tgz#2d400a47e73d0bfc5bc0d3fdf5be487cfdca341b" + integrity sha512-KJt4jhCxqrgGrC02UsQsKw90dPkFknMHsH5HTInT7gkDRRfGFwEd+e2O1/E75br3TdFhvRmzjljYz5thZ58L3A== dependencies: vscode-languageserver-textdocument "^1.0.1" - vscode-languageserver-types "3.16.0-next.2" + vscode-languageserver-types "^3.16.0" vscode-nls "^5.0.0" - vscode-uri "^2.1.2" + vscode-uri "^3.0.2" -vscode-jsonrpc@6.0.0-next.2: - version "6.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0-next.2.tgz#3d73f86d812304cb91b9fb1efee40ec60b09ed7f" - integrity sha512-dKQXRYNUY6BHALQJBJlyZyv9oWlYpbJ2vVoQNNVNPLAYQ3hzNp4zy+iSo7zGx1BPXByArJQDWTKLQh8dz3dnNw== +vscode-jsonrpc@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz#108bdb09b4400705176b957ceca9e0880e9b6d4e" + integrity sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg== -vscode-languageserver-protocol@3.16.0-next.4: - version "3.16.0-next.4" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0-next.4.tgz#8f8b1b831d4dfd9b26aa1ba3d2a32c427a91c99f" - integrity sha512-6GmPUp2MhJy2H1CTWp2B40Pa9BeC9glrXWmQWVG6A/0V9UbcAjVC9m56znm2GL32iyLDIprTBe8gBvvvcjbpaQ== +vscode-languageserver-protocol@3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz#34135b61a9091db972188a07d337406a3cdbe821" + integrity sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A== dependencies: - vscode-jsonrpc "6.0.0-next.2" - vscode-languageserver-types "3.16.0-next.2" + vscode-jsonrpc "6.0.0" + vscode-languageserver-types "3.16.0" vscode-languageserver-textdocument@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz#178168e87efad6171b372add1dea34f53e5d330f" integrity sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA== -vscode-languageserver-types@3.16.0-next.2: - version "3.16.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0-next.2.tgz#940bd15c992295a65eae8ab6b8568a1e8daa3083" - integrity sha512-QjXB7CKIfFzKbiCJC4OWC8xUncLsxo19FzGVp/ADFvvi87PlmBSCAtZI5xwGjF5qE0xkLf0jjKUn3DzmpDP52Q== +vscode-languageserver-types@3.16.0, vscode-languageserver-types@^3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz#ecf393fc121ec6974b2da3efb3155644c514e247" + integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA== -vscode-languageserver@7.0.0-next.3: - version "7.0.0-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-7.0.0-next.3.tgz#3833bd09259a4a085baeba90783f1e4d06d81095" - integrity sha512-qSt8eb546iFuoFIN+9MPl4Avru6Iz2/JP0UmS/3djf40ICa31Np/yJ7anX2j0Az5rCzb0fak8oeKwDioGeVOYg== +vscode-languageserver@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz#49b068c87cfcca93a356969d20f5d9bdd501c6b0" + integrity sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw== dependencies: - vscode-languageserver-protocol "3.16.0-next.4" + vscode-languageserver-protocol "3.16.0" vscode-nls@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840" integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA== -vscode-uri@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-2.1.2.tgz#c8d40de93eb57af31f3c715dd650e2ca2c096f1c" - integrity sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A== - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -which@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -wide-align@1.1.3: - 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" - -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== - dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -xml@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" - integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU= - -y18n@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== - -yargs-parser@13.1.2, yargs-parser@^13.1.1, yargs-parser@^13.1.2: - version "13.1.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" - integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs-unparser@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.0.tgz#ef25c2c769ff6bd09e4b0f9d7c605fb27846ea9f" - integrity sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw== - dependencies: - flat "^4.1.0" - lodash "^4.17.15" - yargs "^13.3.0" - -yargs@13.3.2: - version "13.3.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" - integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.2" - -yargs@^13.3.0: - version "13.3.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83" - integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.1" +vscode-uri@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.2.tgz#ecfd1d066cb8ef4c3a208decdbab9a8c23d055d0" + integrity sha512-jkjy6pjU1fxUvI51P+gCsxg1u2n8LSt0W6KrCNQceaziKzff74GoWmjVG46KieVzybO1sttPQmYfrwSHey7GUA== diff --git a/extensions/css-language-features/yarn.lock b/extensions/css-language-features/yarn.lock index 241dd5e9..eec3195c 100644 --- a/extensions/css-language-features/yarn.lock +++ b/extensions/css-language-features/yarn.lock @@ -2,58 +2,16 @@ # yarn lockfile v1 -"@types/node@^12.11.7": - version "12.11.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.11.7.tgz#57682a9771a3f7b09c2497f28129a0462966524a" - integrity sha512-JNbGaHFCLwgHn/iCckiGSOZ1XYHsKFwREtzPwSGCVld1SGhOlmZw2D4ZI94HQCrBHbADzW9m4LER/8olJTRGHA== - -ansi-colors@3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" - integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw== - -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-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== - -ansi-styles@^3.2.0, 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" - -anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -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" +"@types/node@^12.19.9": + version "12.19.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" + integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== 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= -binary-extensions@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" - integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -62,677 +20,70 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -browser-stdout@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" - integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== - -camelcase@^5.0.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -chalk@^2.0.1: - 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" - -chokidar@3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.0.tgz#12c0714668c55800f659e262d4962a97faf554a6" - integrity sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.2.0" - optionalDependencies: - fsevents "~2.1.1" - -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== - dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.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= - 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= -debug@3.2.6: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== dependencies: - ms "^2.1.1" + yallist "^4.0.0" -decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -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== - dependencies: - object-keys "^1.0.12" - -diff@3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== - -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - -es-abstract@^1.5.1: - version "1.14.2" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.14.2.tgz#7ce108fad83068c8783c3cdf62e504e084d8c497" - integrity sha512-DgoQmbpFNOofkjJtKwr87Ma5EW4Dc8fWhD0R+ndq7Oc456ivUfGOOP6oAZTTKl5/CcNMP+EN+e3/iUzgE0veZg== - dependencies: - es-to-primitive "^1.2.0" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.0" - is-callable "^1.1.4" - is-regex "^1.0.4" - object-inspect "^1.6.0" - object-keys "^1.1.1" - string.prototype.trimleft "^2.0.0" - string.prototype.trimright "^2.0.0" - -es-to-primitive@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" - integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -escape-string-regexp@1.0.5, 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== - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -find-up@3.0.0, find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -flat@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.0.tgz#090bec8b05e39cba309747f1d588f04dbaf98db2" - integrity sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw== - dependencies: - is-buffer "~2.0.3" - -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= - -fsevents@~2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805" - integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -get-caller-file@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -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== - dependencies: - is-glob "^4.0.1" - -glob@7.1.3: - version "7.1.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" - integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== - 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" - -growl@1.10.5: - version "1.10.5" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" - integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== - -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-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" - integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= - -has@^1.0.1, has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -he@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -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: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-buffer@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" - integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw== - -is-callable@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" - integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== - -is-date-object@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" - integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -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= - -is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-regex@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" - integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= - dependencies: - has "^1.0.1" - -is-symbol@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" - integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== - dependencies: - has-symbols "^1.0.0" - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -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" - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -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== - -log-symbols@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" - integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== - dependencies: - chalk "^2.0.1" - -minimatch@3.0.4, minimatch@^3.0.4: +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= - -mkdirp@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= +semver@^7.3.4: + version "7.3.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" + integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== dependencies: - minimist "0.0.8" + lru-cache "^6.0.0" -mocha@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-7.0.1.tgz#276186d35a4852f6249808c6dd4a1376cbf6c6ce" - integrity sha512-9eWmWTdHLXh72rGrdZjNbG3aa1/3NRPpul1z0D979QpEnFdCG0Q5tv834N+94QEN2cysfV72YocQ3fn87s70fg== - dependencies: - ansi-colors "3.2.3" - browser-stdout "1.3.1" - chokidar "3.3.0" - debug "3.2.6" - diff "3.5.0" - escape-string-regexp "1.0.5" - find-up "3.0.0" - glob "7.1.3" - growl "1.10.5" - he "1.2.0" - js-yaml "3.13.1" - log-symbols "2.2.0" - minimatch "3.0.4" - mkdirp "0.5.1" - ms "2.1.1" - node-environment-flags "1.0.6" - object.assign "4.1.0" - strip-json-comments "2.0.1" - supports-color "6.0.0" - which "1.3.1" - wide-align "1.1.3" - yargs "13.3.0" - yargs-parser "13.1.1" - yargs-unparser "1.6.0" - -ms@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - -ms@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -node-environment-flags@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.6.tgz#a30ac13621f6f7d674260a54dede048c3982c088" - integrity sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw== - dependencies: - object.getownpropertydescriptors "^2.0.3" - semver "^5.7.0" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -object-inspect@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b" - integrity sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ== - -object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" - integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== - dependencies: - define-properties "^1.1.2" - function-bind "^1.1.1" - has-symbols "^1.0.0" - object-keys "^1.0.11" - -object.getownpropertydescriptors@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" - integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY= - dependencies: - define-properties "^1.1.2" - es-abstract "^1.5.1" - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -p-limit@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.1.tgz#aa07a788cc3151c939b5131f63570f0dd2009537" - integrity sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg== - dependencies: - p-try "^2.0.0" - -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - -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= - -picomatch@^2.0.4: - version "2.2.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a" - integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA== - -readdirp@~3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.2.0.tgz#c30c33352b12c96dfb4b895421a49fd5a9593839" - integrity sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ== - dependencies: - picomatch "^2.0.4" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -semver@^5.7.0: - version "5.7.1" - 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== - -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= - -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.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-width@^3.0.0, string-width@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - -string.prototype.trimleft@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634" - integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw== - dependencies: - define-properties "^1.1.3" - function-bind "^1.1.1" - -string.prototype.trimright@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58" - integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg== - dependencies: - define-properties "^1.1.3" - function-bind "^1.1.1" - -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-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.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@6.0.0: +vscode-jsonrpc@6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.0.0.tgz#76cfe742cf1f41bb9b1c29ad03068c05b4c0e40a" - integrity sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg== + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz#108bdb09b4400705176b957ceca9e0880e9b6d4e" + integrity sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg== + +vscode-languageclient@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz#b505c22c21ffcf96e167799757fca07a6bad0fb2" + integrity sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg== dependencies: - has-flag "^3.0.0" + minimatch "^3.0.4" + semver "^7.3.4" + vscode-languageserver-protocol "3.16.0" -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== +vscode-languageserver-protocol@3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz#34135b61a9091db972188a07d337406a3cdbe821" + integrity sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A== dependencies: - has-flag "^3.0.0" + vscode-jsonrpc "6.0.0" + vscode-languageserver-types "3.16.0" -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" +vscode-languageserver-types@3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz#ecf393fc121ec6974b2da3efb3155644c514e247" + integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA== -vscode-jsonrpc@6.0.0-next.2: - version "6.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0-next.2.tgz#3d73f86d812304cb91b9fb1efee40ec60b09ed7f" - integrity sha512-dKQXRYNUY6BHALQJBJlyZyv9oWlYpbJ2vVoQNNVNPLAYQ3hzNp4zy+iSo7zGx1BPXByArJQDWTKLQh8dz3dnNw== +vscode-nls@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840" + integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA== -vscode-languageclient@7.0.0-next.5.1: - version "7.0.0-next.5.1" - resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-7.0.0-next.5.1.tgz#ed93f14e4c2cdccedf15002c7bf8ef9cb638f36c" - integrity sha512-OONvbk3IFpubwF8/Y5uPQaq5J5CEskpeET3SfK4iGlv5OUK+44JawH/SEW5wXuEPpfdMLEMZLuGLU5v5d7N7PQ== - dependencies: - semver "^6.3.0" - vscode-languageserver-protocol "3.16.0-next.4" +vscode-uri@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.2.tgz#ecfd1d066cb8ef4c3a208decdbab9a8c23d055d0" + integrity sha512-jkjy6pjU1fxUvI51P+gCsxg1u2n8LSt0W6KrCNQceaziKzff74GoWmjVG46KieVzybO1sttPQmYfrwSHey7GUA== -vscode-languageserver-protocol@3.16.0-next.4: - version "3.16.0-next.4" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0-next.4.tgz#8f8b1b831d4dfd9b26aa1ba3d2a32c427a91c99f" - integrity sha512-6GmPUp2MhJy2H1CTWp2B40Pa9BeC9glrXWmQWVG6A/0V9UbcAjVC9m56znm2GL32iyLDIprTBe8gBvvvcjbpaQ== - dependencies: - vscode-jsonrpc "6.0.0-next.2" - vscode-languageserver-types "3.16.0-next.2" - -vscode-languageserver-types@3.16.0-next.2: - version "3.16.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0-next.2.tgz#940bd15c992295a65eae8ab6b8568a1e8daa3083" - integrity sha512-QjXB7CKIfFzKbiCJC4OWC8xUncLsxo19FzGVp/ADFvvi87PlmBSCAtZI5xwGjF5qE0xkLf0jjKUn3DzmpDP52Q== - -vscode-nls@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.2.tgz#ca8bf8bb82a0987b32801f9fddfdd2fb9fd3c167" - integrity sha512-7bOHxPsfyuCqmP+hZXscLhiHwe7CSuFE4hyhbs22xPIhQ4jv99FcR4eBzfYYVLP356HNFpdvz63FFb/xw6T4Iw== - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -which@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -wide-align@1.1.3: - 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" - -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== - dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -y18n@^4.0.0: +yallist@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== - -yargs-parser@13.1.1, yargs-parser@^13.1.1: - version "13.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" - integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs-unparser@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.0.tgz#ef25c2c769ff6bd09e4b0f9d7c605fb27846ea9f" - integrity sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw== - dependencies: - flat "^4.1.0" - lodash "^4.17.15" - yargs "^13.3.0" - -yargs@13.3.0, yargs@^13.3.0: - version "13.3.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83" - integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== diff --git a/extensions/debug-auto-launch/package.json b/extensions/debug-auto-launch/package.json index f0dc7782..0f349851 100644 --- a/extensions/debug-auto-launch/package.json +++ b/extensions/debug-auto-launch/package.json @@ -29,7 +29,7 @@ "vscode-nls": "^4.0.0" }, "devDependencies": { - "@types/node": "^12.11.7" + "@types/node": "^12.19.9" }, "prettier": { "printWidth": 100, diff --git a/extensions/debug-auto-launch/yarn.lock b/extensions/debug-auto-launch/yarn.lock index 7af62a72..687f15f4 100644 --- a/extensions/debug-auto-launch/yarn.lock +++ b/extensions/debug-auto-launch/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@^12.11.7": - version "12.11.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.11.7.tgz#57682a9771a3f7b09c2497f28129a0462966524a" - integrity sha512-JNbGaHFCLwgHn/iCckiGSOZ1XYHsKFwREtzPwSGCVld1SGhOlmZw2D4ZI94HQCrBHbADzW9m4LER/8olJTRGHA== +"@types/node@^12.19.9": + version "12.19.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" + integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== vscode-nls@^4.0.0: version "4.0.0" diff --git a/extensions/debug-server-ready/package.json b/extensions/debug-server-ready/package.json index 11169906..ac199f05 100644 --- a/extensions/debug-server-ready/package.json +++ b/extensions/debug-server-ready/package.json @@ -4,6 +4,7 @@ "description": "%description%", "version": "1.0.0", "publisher": "vscode", + "license": "MIT", "engines": { "vscode": "^1.32.0" }, @@ -137,6 +138,6 @@ "vscode-nls": "^4.0.0" }, "devDependencies": { - "@types/node": "^12.11.7" + "@types/node": "^12.19.9" } } diff --git a/extensions/debug-server-ready/yarn.lock b/extensions/debug-server-ready/yarn.lock index 7af62a72..687f15f4 100644 --- a/extensions/debug-server-ready/yarn.lock +++ b/extensions/debug-server-ready/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@^12.11.7": - version "12.11.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.11.7.tgz#57682a9771a3f7b09c2497f28129a0462966524a" - integrity sha512-JNbGaHFCLwgHn/iCckiGSOZ1XYHsKFwREtzPwSGCVld1SGhOlmZw2D4ZI94HQCrBHbADzW9m4LER/8olJTRGHA== +"@types/node@^12.19.9": + version "12.19.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" + integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== vscode-nls@^4.0.0: version "4.0.0" diff --git a/extensions/emmet/package.json b/extensions/emmet/package.json index 12178fa8..f3952751 100644 --- a/extensions/emmet/package.json +++ b/extensions/emmet/package.json @@ -17,7 +17,7 @@ "url": "https://github.com/microsoft/vscode-emmet" }, "activationEvents": [ - "*", + "onStartupFinished", "onCommand:emmet.expandAbbreviation", "onLanguage:html", "onLanguage:css", @@ -430,17 +430,16 @@ "deps": "yarn add vscode-emmet-helper" }, "devDependencies": { - "@types/node": "^12.11.7", - "mocha-junit-reporter": "^1.17.0", - "mocha-multi-reporters": "^1.1.7", - "vscode": "1.0.1" + "@types/node": "^12.19.9", + "emmet": "https://github.com/rzhao271/emmet.git#1b2df677d8925ef5ea6da9df8845968403979a0a" }, "dependencies": { + "@emmetio/abbreviation": "^2.2.0", "@emmetio/css-parser": "ramya-rao-a/css-parser#vscode", "@emmetio/html-matcher": "^0.3.3", "@emmetio/math-expression": "^1.0.4", "image-size": "^0.5.2", - "vscode-emmet-helper": "~2.0.0", - "vscode-html-languageservice": "^3.0.3" + "vscode-emmet-helper": "2.2.4", + "vscode-languageserver-textdocument": "^1.0.1" } } diff --git a/extensions/emmet/src/abbreviationActions.ts b/extensions/emmet/src/abbreviationActions.ts index a37741b8..494bf8d2 100644 --- a/extensions/emmet/src/abbreviationActions.ts +++ b/extensions/emmet/src/abbreviationActions.ts @@ -4,16 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { Node, HtmlNode, Rule, Property, Stylesheet } from 'EmmetNode'; -import { getEmmetHelper, getNode, getInnerRange, getMappingForIncludedLanguages, parseDocument, validate, getEmmetConfiguration, isStyleSheet, getEmmetMode, parsePartialStylesheet, isStyleAttribute, getEmbeddedCssNodeIfAny, allowedMimeTypesInScriptTag, toLSTextDocument } from './util'; +import * as nls from 'vscode-nls'; +import { Node, HtmlNode, Rule, Property, Stylesheet } from 'EmmetFlatNode'; +import { getEmmetHelper, getFlatNode, getMappingForIncludedLanguages, validate, getEmmetConfiguration, isStyleSheet, getEmmetMode, parsePartialStylesheet, isStyleAttribute, getEmbeddedCssNodeIfAny, allowedMimeTypesInScriptTag, toLSTextDocument } from './util'; +import { getRootNode as parseDocument } from './parseDocument'; +import { MarkupAbbreviation } from 'emmet'; +// import { AbbreviationNode } from '@emmetio/abbreviation'; +const localize = nls.loadMessageBundle(); const trimRegex = /[\u00a0]*[\d#\-\*\u2022]+\.?/; const hexColorRegex = /^#[\da-fA-F]{0,6}$/; -const inlineElements = ['a', 'abbr', 'acronym', 'applet', 'b', 'basefont', 'bdo', - 'big', 'br', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', - 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'map', 'object', 'q', - 's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', - 'textarea', 'tt', 'u', 'var']; +// const inlineElements = ['a', 'abbr', 'acronym', 'applet', 'b', 'basefont', 'bdo', +// 'big', 'br', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', +// 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'map', 'object', 'q', +// 's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', +// 'textarea', 'tt', 'u', 'var']; interface ExpandAbbreviationInput { syntax: string; @@ -31,35 +36,28 @@ interface PreviewRangesWithContent { } export function wrapWithAbbreviation(args: any) { - return doWrapping(false, args); + return doWrapping(true, args); } export function wrapIndividualLinesWithAbbreviation(args: any) { return doWrapping(true, args); } -function doWrapping(individualLines: boolean, args: any) { +function doWrapping(_: boolean, args: any) { if (!validate(false) || !vscode.window.activeTextEditor) { return; } const editor = vscode.window.activeTextEditor; - if (individualLines) { - if (editor.selections.length === 1 && editor.selection.isEmpty) { - vscode.window.showInformationMessage('Select more than 1 line and try again.'); - return; - } - if (editor.selections.find(x => x.isEmpty)) { - vscode.window.showInformationMessage('Select more than 1 line in each selection and try again.'); - return; - } - } + const document = editor.document; + args = args || {}; if (!args['language']) { - args['language'] = editor.document.languageId; + args['language'] = document.languageId; } + // we know it's not stylesheet due to the validate(false) call above const syntax = getSyntaxFromArgs(args) || 'html'; - const rootNode = parseDocument(editor.document, false); + const rootNode = parseDocument(document, true); let inPreview = false; let currentValue = ''; @@ -69,34 +67,49 @@ function doWrapping(individualLines: boolean, args: any) { const rangesToReplace: PreviewRangesWithContent[] = editor.selections.sort((a: vscode.Selection, b: vscode.Selection) => { return a.start.compareTo(b.start); }).map(selection => { let rangeToReplace: vscode.Range = selection.isReversed ? new vscode.Range(selection.active, selection.anchor) : selection; if (!rangeToReplace.isSingleLine && rangeToReplace.end.character === 0) { + // in case of multi-line, exclude last empty line from rangeToReplace const previousLine = rangeToReplace.end.line - 1; - const lastChar = editor.document.lineAt(previousLine).text.length; + const lastChar = document.lineAt(previousLine).text.length; rangeToReplace = new vscode.Range(rangeToReplace.start, new vscode.Position(previousLine, lastChar)); } else if (rangeToReplace.isEmpty) { const { active } = selection; - const currentNode = getNode(rootNode, active, true); - if (currentNode && (currentNode.start.line === active.line || currentNode.end.line === active.line)) { - rangeToReplace = new vscode.Range(currentNode.start, currentNode.end); + const activeOffset = document.offsetAt(active); + const currentNode = getFlatNode(rootNode, activeOffset, true); + if (currentNode) { + const currentNodeStart = document.positionAt(currentNode.start); + const currentNodeEnd = document.positionAt(currentNode.end); + if (currentNodeStart.line === active.line || currentNodeEnd.line === active.line) { + // wrap around entire node + rangeToReplace = new vscode.Range(currentNodeStart, currentNodeEnd); + } + else { + // wrap line that cursor is on + rangeToReplace = new vscode.Range(rangeToReplace.start.line, 0, rangeToReplace.start.line, document.lineAt(rangeToReplace.start.line).text.length); + } } else { - rangeToReplace = new vscode.Range(rangeToReplace.start.line, 0, rangeToReplace.start.line, editor.document.lineAt(rangeToReplace.start.line).text.length); + // wrap line that cursor is on + rangeToReplace = new vscode.Range(rangeToReplace.start.line, 0, rangeToReplace.start.line, document.lineAt(rangeToReplace.start.line).text.length); } } - const firstLineOfSelection = editor.document.lineAt(rangeToReplace.start).text.substr(rangeToReplace.start.character); + const firstLineOfSelection = document.lineAt(rangeToReplace.start).text.substr(rangeToReplace.start.character); const matches = firstLineOfSelection.match(/^(\s*)/); const extraWhitespaceSelected = matches ? matches[1].length : 0; rangeToReplace = new vscode.Range(rangeToReplace.start.line, rangeToReplace.start.character + extraWhitespaceSelected, rangeToReplace.end.line, rangeToReplace.end.character); let textToWrapInPreview: string[]; - const textToReplace = editor.document.getText(rangeToReplace); - if (individualLines) { - textToWrapInPreview = textToReplace.split('\n').map(x => x.trim()); - } else { - const wholeFirstLine = editor.document.lineAt(rangeToReplace.start).text; - const otherMatches = wholeFirstLine.match(/^(\s*)/); - const precedingWhitespace = otherMatches ? otherMatches[1] : ''; - textToWrapInPreview = rangeToReplace.isSingleLine ? [textToReplace] : ['\n\t' + textToReplace.split('\n' + precedingWhitespace).join('\n\t') + '\n']; - } + const textToReplace = document.getText(rangeToReplace); + + // the following assumes all the lines are indented the same way as the first + // this assumption helps with applyPreview later + const wholeFirstLine = document.lineAt(rangeToReplace.start).text; + const otherMatches = wholeFirstLine.match(/^(\s*)/); + const precedingWhitespace = otherMatches ? otherMatches[1] : ''; + textToWrapInPreview = rangeToReplace.isSingleLine ? + [textToReplace] : + textToReplace.split('\n' + precedingWhitespace).map(x => x.trimEnd()); + + // escape $ characters, fixes #52640 textToWrapInPreview = textToWrapInPreview.map(e => e.replace(/(\$\d)/g, '\\$1')); return { @@ -107,7 +120,29 @@ function doWrapping(individualLines: boolean, args: any) { }; }); - function revertPreview(): Thenable { + // if a selection falls on a node, it could interfere with linked editing, + // so back up the selections, and change selections to wrap around the node + const oldSelections = editor.selections; + const newSelections: vscode.Selection[] = []; + editor.selections.forEach(selection => { + let { start, end } = selection; + const startOffset = document.offsetAt(start); + const startNode = getFlatNode(rootNode, startOffset, true); + const endOffset = document.offsetAt(end); + const endNode = getFlatNode(rootNode, endOffset, true); + if (startNode) { + start = document.positionAt(startNode.start); + } + if (endNode) { + end = document.positionAt(endNode.end); + } + // don't need to preserve active/anchor order since the selection changes + // after wrapping anyway + newSelections.push(new vscode.Selection(start, end)); + }); + editor.selections = newSelections; + + function revertPreview(): Thenable { return editor.edit(builder => { for (const rangeToReplace of rangesToReplace) { builder.replace(rangeToReplace.previewRange, rangeToReplace.originalContent); @@ -119,9 +154,10 @@ function doWrapping(individualLines: boolean, args: any) { function applyPreview(expandAbbrList: ExpandAbbreviationInput[]): Thenable { let lastOldPreviewRange = new vscode.Range(0, 0, 0, 0); let lastNewPreviewRange = new vscode.Range(0, 0, 0, 0); - let totalLinesInserted = 0; + let totalNewLinesInserted = 0; return editor.edit(builder => { + // the edits are applied in order top-down for (let i = 0; i < rangesToReplace.length; i++) { const expandedText = expandAbbr(expandAbbrList[i]) || ''; if (!expandedText) { @@ -129,11 +165,14 @@ function doWrapping(individualLines: boolean, args: any) { break; } + // get the current preview range, format the new wrapped text, and then replace + // the text in the preview range with that new text const oldPreviewRange = rangesToReplace[i].previewRange; const preceedingText = editor.document.getText(new vscode.Range(oldPreviewRange.start.line, 0, oldPreviewRange.start.line, oldPreviewRange.start.character)); const indentPrefix = (preceedingText.match(/^(\s*)/) || ['', ''])[1]; - let newText = expandedText.replace(/\n/g, '\n' + indentPrefix); // Adding indentation on each line of expanded text + let newText = expandedText; + newText = newText.replace(/\n/g, '\n' + indentPrefix); // Adding indentation on each line of expanded text newText = newText.replace(/\$\{[\d]*\}/g, '|'); // Removing Tabstops newText = newText.replace(/\$\{[\d]*(:[^}]*)?\}/g, (match) => { // Replacing Placeholders return match.replace(/^\$\{[\d]*:/, '').replace('}', ''); @@ -141,13 +180,17 @@ function doWrapping(individualLines: boolean, args: any) { newText = newText.replace(/\\\$/g, '$'); // Remove backslashes before $ builder.replace(oldPreviewRange, newText); + // calculate the new preview range to use for future previews + // we also have to take into account that the previous expansions could: + // - cause new lines to appear + // - be on the same line as other expansions const expandedTextLines = newText.split('\n'); const oldPreviewLines = oldPreviewRange.end.line - oldPreviewRange.start.line + 1; const newLinesInserted = expandedTextLines.length - oldPreviewLines; - const newPreviewLineStart = oldPreviewRange.start.line + totalLinesInserted; + const newPreviewLineStart = oldPreviewRange.start.line + totalNewLinesInserted; let newPreviewStart = oldPreviewRange.start.character; - const newPreviewLineEnd = oldPreviewRange.end.line + totalLinesInserted + newLinesInserted; + const newPreviewLineEnd = oldPreviewRange.end.line + totalNewLinesInserted + newLinesInserted; let newPreviewEnd = expandedTextLines[expandedTextLines.length - 1].length; if (i > 0 && newPreviewLineEnd === lastNewPreviewRange.end.line) { // If newPreviewLineEnd is equal to the previous expandedText lineEnd, @@ -166,9 +209,9 @@ function doWrapping(individualLines: boolean, args: any) { } lastOldPreviewRange = rangesToReplace[i].previewRange; - rangesToReplace[i].previewRange = lastNewPreviewRange = new vscode.Range(newPreviewLineStart, newPreviewStart, newPreviewLineEnd, newPreviewEnd); - - totalLinesInserted += newLinesInserted; + lastNewPreviewRange = new vscode.Range(newPreviewLineStart, newPreviewStart, newPreviewLineEnd, newPreviewEnd); + rangesToReplace[i].previewRange = lastNewPreviewRange; + totalNewLinesInserted += newLinesInserted; } }, { undoStopBefore: false, undoStopAfter: false }); } @@ -187,19 +230,21 @@ function doWrapping(individualLines: boolean, args: any) { const { abbreviation, filter } = extractedResults; if (definitive) { - const revertPromise = inPreview ? revertPreview() : Promise.resolve(); + const revertPromise = inPreview ? revertPreview() : Promise.resolve(true); return revertPromise.then(() => { const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent => { const rangeToReplace = rangesAndContent.originalRange; let textToWrap: string[]; - if (individualLines) { - textToWrap = rangesAndContent.textToWrapInPreview; - } else { - textToWrap = rangeToReplace.isSingleLine ? ['$TM_SELECTED_TEXT'] : ['\n\t$TM_SELECTED_TEXT\n']; - } + // if (individualLines) { + textToWrap = rangesAndContent.textToWrapInPreview; + // } else { + // // use the p tag as a dummy element to get Emmet to wrap the expression properly + // textToWrap = rangeToReplace.isSingleLine ? + // ['$TM_SELECTED_TEXT'] : ['

$TM_SELECTED_TEXT

']; + // } return { syntax: syntax || '', abbreviation, rangeToReplace, textToWrap, filter }; }); - return expandAbbreviationInRange(editor, expandAbbrList, !individualLines).then(() => { return true; }); + return expandAbbreviationInRange(editor, expandAbbrList, false).then(() => { return true; }); }); } @@ -214,16 +259,22 @@ function doWrapping(individualLines: boolean, args: any) { if (value !== currentValue) { currentValue = value; makeChanges(value, false).then((out) => { - if (typeof out === 'boolean') { - inPreview = out; - } + inPreview = out; }); } return ''; } - const abbreviationPromise: Thenable = (args && args['abbreviation']) ? Promise.resolve(args['abbreviation']) : vscode.window.showInputBox({ prompt: 'Enter Abbreviation', validateInput: inputChanged }); - return abbreviationPromise.then(inputAbbreviation => { - return makeChanges(inputAbbreviation, true); + + const prompt = localize('wrapWithAbbreviationPrompt', "Enter Abbreviation"); + const abbreviationPromise: Thenable = (args && args['abbreviation']) ? + Promise.resolve(args['abbreviation']) : + vscode.window.showInputBox({ prompt, validateInput: inputChanged }); + return abbreviationPromise.then(async (inputAbbreviation) => { + const changesWereMade = await makeChanges(inputAbbreviation, true); + if (!changesWereMade) { + editor.selections = oldSelections; + } + return changesWereMade; }); } @@ -327,7 +378,7 @@ export function expandEmmetAbbreviation(args: any): Thenable 1000) { rootNode = parsePartialStylesheet(editor.document, editor.selection.isReversed ? editor.selection.anchor : editor.selection.active); } else { - rootNode = parseDocument(editor.document, false); + rootNode = parseDocument(editor.document, true); } return rootNode; @@ -342,24 +393,25 @@ export function expandEmmetAbbreviation(args: any): Thenable { * @param position position to validate * @param abbreviationRange The range of the abbreviation for which given position is being validated */ -export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocument, rootNode: Node | undefined, currentNode: Node | null, syntax: string, position: vscode.Position, abbreviationRange: vscode.Range): boolean { +export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocument, rootNode: Node | undefined, currentNode: Node | undefined, syntax: string, offset: number, abbreviationRange: vscode.Range): boolean { if (isStyleSheet(syntax)) { const stylesheet = rootNode; - if (stylesheet && (stylesheet.comments || []).some(x => position.isAfterOrEqual(x.start) && position.isBeforeOrEqual(x.end))) { + if (stylesheet && (stylesheet.comments || []).some(x => offset >= x.start && offset <= x.end)) { return false; } // Continue validation only if the file was parse-able and the currentNode has been found @@ -419,14 +471,14 @@ export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocumen const propertyNode = currentNode; if (propertyNode.terminatorToken && propertyNode.separator - && position.isAfterOrEqual(propertyNode.separatorToken.end) - && position.isBeforeOrEqual(propertyNode.terminatorToken.start) + && offset >= propertyNode.separatorToken.end + && offset <= propertyNode.terminatorToken.start && abbreviation.indexOf(':') === -1) { return hexColorRegex.test(abbreviation) || abbreviation === '!'; } if (!propertyNode.terminatorToken && propertyNode.separator - && position.isAfterOrEqual(propertyNode.separatorToken.end) + && offset >= propertyNode.separatorToken.end && abbreviation.indexOf(':') === -1) { return hexColorRegex.test(abbreviation) || abbreviation === '!'; } @@ -444,7 +496,7 @@ export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocumen const currentCssNode = currentNode; // Position is valid if it occurs after the `{` that marks beginning of rule contents - if (position.isAfter(currentCssNode.contentStartToken.end)) { + if (offset > currentCssNode.contentStartToken.end) { return true; } @@ -453,12 +505,16 @@ export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocumen // But we should assume it is a valid location for css properties under the parent rule if (currentCssNode.parent && (currentCssNode.parent.type === 'rule' || currentCssNode.parent.type === 'at-rule') - && currentCssNode.selectorToken - && position.line !== currentCssNode.selectorToken.end.line - && currentCssNode.selectorToken.start.character === abbreviationRange.start.character - && currentCssNode.selectorToken.start.line === abbreviationRange.start.line - ) { - return true; + && currentCssNode.selectorToken) { + const position = document.positionAt(offset); + const tokenStartPos = document.positionAt(currentCssNode.selectorToken.start); + const tokenEndPos = document.positionAt(currentCssNode.selectorToken.end); + if (position.line !== tokenEndPos.line + && tokenStartPos.character === abbreviationRange.start.character + && tokenStartPos.line === abbreviationRange.start.line + ) { + return true; + } } return false; @@ -469,7 +525,7 @@ export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocumen const escape = '\\'; const question = '?'; const currentHtmlNode = currentNode; - let start = new vscode.Position(0, 0); + let start = 0; if (currentHtmlNode) { if (currentHtmlNode.name === 'script') { @@ -487,27 +543,27 @@ export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocumen return false; } - const innerRange = getInnerRange(currentHtmlNode); - // Fix for https://github.com/microsoft/vscode/issues/28829 - if (!innerRange || !innerRange.contains(position)) { + if (!currentHtmlNode.open || !currentHtmlNode.close || + !(currentHtmlNode.open.end <= offset && offset <= currentHtmlNode.close.start)) { return false; } // Fix for https://github.com/microsoft/vscode/issues/35128 // Find the position up till where we will backtrack looking for unescaped < or > // to decide if current position is valid for emmet expansion - start = innerRange.start; + start = currentHtmlNode.open.end; let lastChildBeforePosition = currentHtmlNode.firstChild; while (lastChildBeforePosition) { - if (lastChildBeforePosition.end.isAfter(position)) { + if (lastChildBeforePosition.end > offset) { break; } start = lastChildBeforePosition.end; lastChildBeforePosition = lastChildBeforePosition.nextSibling; } } - let textToBackTrack = document.getText(new vscode.Range(start.line, start.character, abbreviationRange.start.line, abbreviationRange.start.character)); + const startPos = document.positionAt(start); + let textToBackTrack = document.getText(new vscode.Range(startPos.line, startPos.character, abbreviationRange.start.line, abbreviationRange.start.character)); // Worse case scenario is when cursor is inside a big chunk of text which needs to backtracked // Backtrack only 500 offsets to ensure we dont waste time doing this @@ -582,7 +638,7 @@ function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrList: Ex const insertPromises: Thenable[] = []; if (!insertSameSnippet) { expandAbbrList.sort((a: ExpandAbbreviationInput, b: ExpandAbbreviationInput) => { return b.rangeToReplace.start.compareTo(a.rangeToReplace.start); }).forEach((expandAbbrInput: ExpandAbbreviationInput) => { - let expandedText = expandAbbr(expandAbbrInput); + const expandedText = expandAbbr(expandAbbrInput); if (expandedText) { insertPromises.push(editor.insertSnippet(new vscode.SnippetString(expandedText), expandAbbrInput.rangeToReplace, { undoStopBefore: false, undoStopAfter: false })); } @@ -607,24 +663,26 @@ function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrList: Ex return Promise.resolve(false); } -/* -* Walks the tree rooted at root and apply function fn on each node. -* if fn return false at any node, the further processing of tree is stopped. -*/ -function walk(root: any, fn: ((node: any) => boolean)): boolean { - let ctx = root; - while (ctx) { +// /* +// * Walks the tree rooted at root and apply function fn on each node. +// * if fn return false at any node, the further processing of tree is stopped. +// */ +// function walk(root: AbbreviationNode, fn: ((node: AbbreviationNode) => boolean)): boolean { +// if (fn(root) === false || walkChildren(root.children, fn) === false) { +// return false; +// } +// return true; +// } - const next = ctx.next; - if (fn(ctx) === false || walk(ctx.firstChild, fn) === false) { - return false; - } - - ctx = next; - } - - return true; -} +// function walkChildren(children: AbbreviationNode[], fn: ((node: AbbreviationNode) => boolean)): boolean { +// for (let i = 0; i < children.length; i++) { +// const child = children[i]; +// if (walk(child, fn) === false) { +// return false; +// } +// } +// return true; +// } /** * Expands abbreviation as detailed in given input. @@ -634,7 +692,7 @@ function expandAbbr(input: ExpandAbbreviationInput): string | undefined { const expandOptions = helper.getExpandOptions(input.syntax, getEmmetConfiguration(input.syntax), input.filter); if (input.textToWrap) { - if (input.filter && input.filter.indexOf('t') > -1) { + if (input.filter && input.filter.includes('t')) { input.textToWrap = input.textToWrap.map(line => { return line.replace(trimRegex, '').trim(); }); @@ -644,46 +702,47 @@ function expandAbbr(input: ExpandAbbreviationInput): string | undefined { // Below fixes https://github.com/microsoft/vscode/issues/29898 // With this, Emmet formats inline elements as block elements // ensuring the wrapped multi line text does not get merged to a single line - if (!input.rangeToReplace.isSingleLine) { - expandOptions.profile['inlineBreak'] = 1; + if (!input.rangeToReplace.isSingleLine && expandOptions.options) { + expandOptions.options['output.inlineBreak'] = 1; } } let expandedText; try { // Expand the abbreviation + if (input.textToWrap && !isStyleSheet(input.syntax)) { + const parsedAbbr = helper.parseAbbreviation(input.abbreviation, expandOptions); + // if (input.rangeToReplace.isSingleLine && input.textToWrap.length === 1) { + // // Fetch rightmost element in the parsed abbreviation (i.e the element that will contain the wrapped text). + // const wrappingNodeChildren = parsedAbbr.children; + // let wrappingNode = wrappingNodeChildren[wrappingNodeChildren.length - 1]; + // while (wrappingNode && wrappingNode.children && wrappingNode.children.length > 0) { + // wrappingNode = wrappingNode.children[wrappingNode.children.length - 1]; + // } - if (input.textToWrap) { - const parsedAbbr = helper.parseAbbreviation(input.abbreviation, expandOptions); - if (input.rangeToReplace.isSingleLine && input.textToWrap.length === 1) { - - // Fetch rightmost element in the parsed abbreviation (i.e the element that will contain the wrapped text). - let wrappingNode = parsedAbbr; - while (wrappingNode && wrappingNode.children && wrappingNode.children.length > 0) { - wrappingNode = wrappingNode.children[wrappingNode.children.length - 1]; - } - - // If wrapping with a block element, insert newline in the text to wrap. - if (wrappingNode && inlineElements.indexOf(wrappingNode.name) === -1 && (expandOptions['profile'].hasOwnProperty('format') ? expandOptions['profile'].format : true)) { - wrappingNode.value = '\n\t' + wrappingNode.value + '\n'; - } - } + // // If wrapping with a block element, insert newline in the text to wrap. + // // const format = expandOptions.options ? (expandOptions.options['output.format'] ?? true) : true; + // // if (wrappingNode && wrappingNode.name && wrappingNode.value + // // && inlineElements.indexOf(wrappingNode.name) === -1 + // // && format) { + // // wrappingNode.value[0] = '\n\t' + wrappingNode.value[0] + '\n'; + // // } + // } // Below fixes https://github.com/microsoft/vscode/issues/78219 // walk the tree and remove tags for empty values - walk(parsedAbbr, node => { - if (node.name !== null && node.value === '' && !node.isSelfClosing && node.children.length === 0) { - node.name = ''; - node.value = '\n'; - } - - return true; - }); + // walkChildren(parsedAbbr.children, node => { + // if (node.name !== null && node.value && node.value[0] === '' && !node.selfClosing && node.children.length === 0) { + // node.name = ''; + // node.value[0] = '\n'; + // } + // return true; + // }); expandedText = helper.expandAbbreviation(parsedAbbr, expandOptions); // All $anyword would have been escaped by the emmet helper. // Remove the escaping backslash from $TM_SELECTED_TEXT so that VS Code Snippet controller can treat it as a variable - expandedText = expandedText.replace('\\$TM_SELECTED_TEXT', '$TM_SELECTED_TEXT'); + expandedText = expandedText.replace('

\\$TM_SELECTED_TEXT

', '$TM_SELECTED_TEXT'); } else { expandedText = helper.expandAbbreviation(input.abbreviation, expandOptions); } diff --git a/extensions/emmet/src/balance.ts b/extensions/emmet/src/balance.ts index 21ac3027..9cce9593 100644 --- a/extensions/emmet/src/balance.ts +++ b/extensions/emmet/src/balance.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { HtmlNode } from 'EmmetNode'; -import { getHtmlNode, parseDocument, validate } from './util'; +import { getHtmlFlatNode, offsetRangeToSelection, validate } from './util'; +import { getRootNode } from './parseDocument'; +import { HtmlNode as HtmlFlatNode } from 'EmmetFlatNode'; let balanceOutStack: Array = []; -let lastOut = false; let lastBalancedSelections: vscode.Selection[] = []; export function balanceOut() { @@ -24,53 +24,51 @@ function balance(out: boolean) { return; } const editor = vscode.window.activeTextEditor; - let rootNode = parseDocument(editor.document); + const document = editor.document; + const rootNode = getRootNode(document, true); if (!rootNode) { return; } - let getRangeFunction = out ? getRangeToBalanceOut : getRangeToBalanceIn; + const rangeFn = out ? getRangeToBalanceOut : getRangeToBalanceIn; let newSelections: vscode.Selection[] = []; editor.selections.forEach(selection => { - let range = getRangeFunction(editor.document, selection, rootNode); + const range = rangeFn(document, rootNode, selection); newSelections.push(range); }); - if (areSameSelections(newSelections, editor.selections)) { - return; - } - + // check whether we are starting a balance elsewhere if (areSameSelections(lastBalancedSelections, editor.selections)) { + // we are not starting elsewhere, so use the stack as-is if (out) { - if (!balanceOutStack.length) { + // make sure we are able to expand outwards + if (!areSameSelections(editor.selections, newSelections)) { balanceOutStack.push(editor.selections); } - balanceOutStack.push(newSelections); - } else { - if (lastOut) { - balanceOutStack.pop(); - } - newSelections = balanceOutStack.pop() || newSelections; + } else if (balanceOutStack.length) { + newSelections = balanceOutStack.pop()!; } } else { - balanceOutStack = out ? [editor.selections, newSelections] : []; + // we are starting elsewhere, so reset the stack + balanceOutStack = out ? [editor.selections] : []; } - lastOut = out; - lastBalancedSelections = editor.selections = newSelections; + editor.selections = newSelections; + lastBalancedSelections = editor.selections; } -function getRangeToBalanceOut(document: vscode.TextDocument, selection: vscode.Selection, rootNode: HtmlNode): vscode.Selection { - let nodeToBalance = getHtmlNode(document, rootNode, selection.start, false); +function getRangeToBalanceOut(document: vscode.TextDocument, rootNode: HtmlFlatNode, selection: vscode.Selection): vscode.Selection { + const offset = document.offsetAt(selection.start); + const nodeToBalance = getHtmlFlatNode(document.getText(), rootNode, offset, false); if (!nodeToBalance) { return selection; } - if (!nodeToBalance.close) { - return new vscode.Selection(nodeToBalance.start, nodeToBalance.end); + if (!nodeToBalance.open || !nodeToBalance.close) { + return offsetRangeToSelection(document, nodeToBalance.start, nodeToBalance.end); } - let innerSelection = new vscode.Selection(nodeToBalance.open.end, nodeToBalance.close.start); - let outerSelection = new vscode.Selection(nodeToBalance.start, nodeToBalance.end); + const innerSelection = offsetRangeToSelection(document, nodeToBalance.open.end, nodeToBalance.close.start); + const outerSelection = offsetRangeToSelection(document, nodeToBalance.open.start, nodeToBalance.close.end); if (innerSelection.contains(selection) && !innerSelection.isEqual(selection)) { return innerSelection; @@ -81,19 +79,22 @@ function getRangeToBalanceOut(document: vscode.TextDocument, selection: vscode.S return selection; } -function getRangeToBalanceIn(document: vscode.TextDocument, selection: vscode.Selection, rootNode: HtmlNode): vscode.Selection { - let nodeToBalance = getHtmlNode(document, rootNode, selection.start, true); +function getRangeToBalanceIn(document: vscode.TextDocument, rootNode: HtmlFlatNode, selection: vscode.Selection): vscode.Selection { + const offset = document.offsetAt(selection.start); + const nodeToBalance = getHtmlFlatNode(document.getText(), rootNode, offset, true); if (!nodeToBalance) { return selection; } - if (nodeToBalance.close) { - const entireNodeSelected = selection.start.isEqual(nodeToBalance.start) && selection.end.isEqual(nodeToBalance.end); - const startInOpenTag = selection.start.isAfter(nodeToBalance.open.start) && selection.start.isBefore(nodeToBalance.open.end); - const startInCloseTag = selection.start.isAfter(nodeToBalance.close.start) && selection.start.isBefore(nodeToBalance.close.end); + const selectionStart = document.offsetAt(selection.start); + const selectionEnd = document.offsetAt(selection.end); + if (nodeToBalance.open && nodeToBalance.close) { + const entireNodeSelected = selectionStart === nodeToBalance.start && selectionEnd === nodeToBalance.end; + const startInOpenTag = selectionStart > nodeToBalance.open.start && selectionStart < nodeToBalance.open.end; + const startInCloseTag = selectionStart > nodeToBalance.close.start && selectionStart < nodeToBalance.close.end; if (entireNodeSelected || startInOpenTag || startInCloseTag) { - return new vscode.Selection(nodeToBalance.open.end, nodeToBalance.close.start); + return offsetRangeToSelection(document, nodeToBalance.open.end, nodeToBalance.close.start); } } @@ -101,14 +102,15 @@ function getRangeToBalanceIn(document: vscode.TextDocument, selection: vscode.Se return selection; } - if (selection.start.isEqual(nodeToBalance.firstChild.start) - && selection.end.isEqual(nodeToBalance.firstChild.end) - && nodeToBalance.firstChild.close) { - return new vscode.Selection(nodeToBalance.firstChild.open.end, nodeToBalance.firstChild.close.start); + const firstChild = nodeToBalance.firstChild; + if (selectionStart === firstChild.start + && selectionEnd === firstChild.end + && firstChild.open + && firstChild.close) { + return offsetRangeToSelection(document, firstChild.open.end, firstChild.close.start); } - return new vscode.Selection(nodeToBalance.firstChild.start, nodeToBalance.firstChild.end); - + return offsetRangeToSelection(document, firstChild.start, firstChild.end); } function areSameSelections(a: vscode.Selection[], b: vscode.Selection[]): boolean { @@ -121,4 +123,4 @@ function areSameSelections(a: vscode.Selection[], b: vscode.Selection[]): boolea } } return true; -} \ No newline at end of file +} diff --git a/extensions/emmet/src/bufferStream.ts b/extensions/emmet/src/bufferStream.ts index 81702969..60376c1e 100644 --- a/extensions/emmet/src/bufferStream.ts +++ b/extensions/emmet/src/bufferStream.ts @@ -5,7 +5,7 @@ /* Based on @sergeche's work in his emmet plugin */ -import { TextDocument, Position, Range, EndOfLine } from 'vscode'; +import { TextDocument } from 'vscode'; /** * A stream reader for VSCode's `TextDocument` @@ -13,40 +13,37 @@ import { TextDocument, Position, Range, EndOfLine } from 'vscode'; */ export class DocumentStreamReader { private document: TextDocument; - private start: Position; - private _eof: Position; - private _sof: Position; - public pos: Position; - private _eol: string; - - constructor(document: TextDocument, pos?: Position, limit?: Range) { + private start: number; + private _eof: number; + private _sof: number; + public pos: number; + constructor(document: TextDocument, pos?: number, limit?: [number, number]) { this.document = document; - this.start = this.pos = pos ? pos : new Position(0, 0); - this._sof = limit ? limit.start : new Position(0, 0); - this._eof = limit ? limit.end : new Position(this.document.lineCount - 1, this._lineLength(this.document.lineCount - 1)); - this._eol = this.document.eol === EndOfLine.LF ? '\n' : '\r\n'; + this.start = this.pos = pos ? pos : 0; + this._sof = limit ? limit[0] : 0; + this._eof = limit ? limit[1] : document.getText().length; } /** * Returns true only if the stream is at the start of the file. */ sof(): boolean { - return this.pos.isBeforeOrEqual(this._sof); + return this.pos <= this._sof; } /** * Returns true only if the stream is at the end of the file. */ eof(): boolean { - return this.pos.isAfterOrEqual(this._eof); + return this.pos >= this._eof; } /** * Creates a new stream instance which is limited to given range for given document */ - limit(start: Position, end: Position): DocumentStreamReader { - return new DocumentStreamReader(this.document, start, new Range(start, end)); + limit(start: number, end: number): DocumentStreamReader { + return new DocumentStreamReader(this.document, start, [start, end]); } /** @@ -57,8 +54,7 @@ export class DocumentStreamReader { if (this.eof()) { return NaN; } - const line = this.document.lineAt(this.pos.line).text; - return this.pos.character < line.length ? line.charCodeAt(this.pos.character) : this._eol.charCodeAt(this.pos.character - line.length); + return this.document.getText().charCodeAt(this.pos); } /** @@ -70,19 +66,12 @@ export class DocumentStreamReader { return NaN; } - const line = this.document.lineAt(this.pos.line).text; - let code: number; - if (this.pos.character < line.length) { - code = line.charCodeAt(this.pos.character); - this.pos = this.pos.translate(0, 1); - } else { - code = this._eol.charCodeAt(this.pos.character - line.length); - this.pos = new Position(this.pos.line + 1, 0); - } + const code = this.document.getText().charCodeAt(this.pos); + this.pos++; if (this.eof()) { // restrict pos to eof, if in case it got moved beyond eof - this.pos = new Position(this._eof.line, this._eof.character); + this.pos = this._eof; } return code; @@ -92,20 +81,11 @@ export class DocumentStreamReader { * Backs up the stream n characters. Backing it up further than the * start of the current token will cause things to break, so be careful. */ - backUp(n: number) { - let row = this.pos.line; - let column = this.pos.character; - column -= (n || 1); - - while (row >= 0 && column < 0) { - row--; - column += this._lineLength(row); + backUp(n: number): number { + this.pos -= n; + if (this.pos < 0) { + this.pos = 0; } - - this.pos = row < 0 || column < 0 - ? new Position(0, 0) - : new Position(row, column); - return this.peek(); } @@ -120,29 +100,18 @@ export class DocumentStreamReader { /** * Returns contents for given range */ - substring(from: Position, to: Position): string { - return this.document.getText(new Range(from, to)); + substring(from: number, to: number): string { + return this.document.getText().substring(from, to); } /** * Creates error object with current stream state */ error(message: string): Error { - const err = new Error(`${message} at row ${this.pos.line}, column ${this.pos.character}`); - + const err = new Error(`${message} at offset ${this.pos}`); return err; } - /** - * Returns line length of given row, including line ending - */ - _lineLength(row: number): number { - if (row === this.document.lineCount - 1) { - return this.document.lineAt(row).text.length; - } - return this.document.lineAt(row).text.length + this._eol.length; - } - /** * `match` can be a character code or a function that takes a character code * and returns a boolean. If the next character in the stream 'matches' @@ -167,6 +136,6 @@ export class DocumentStreamReader { eatWhile(match: number | Function): boolean { const start = this.pos; while (!this.eof() && this.eat(match)) { } - return !this.pos.isEqual(start); + return this.pos !== start; } } diff --git a/extensions/emmet/src/defaultCompletionProvider.ts b/extensions/emmet/src/defaultCompletionProvider.ts index 9ac469af..bb9226c5 100644 --- a/extensions/emmet/src/defaultCompletionProvider.ts +++ b/extensions/emmet/src/defaultCompletionProvider.ts @@ -4,17 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { Node, Stylesheet } from 'EmmetNode'; +import { Node, Stylesheet } from 'EmmetFlatNode'; import { isValidLocationForEmmetAbbreviation, getSyntaxFromArgs } from './abbreviationActions'; -import { getEmmetHelper, getMappingForIncludedLanguages, parsePartialStylesheet, getEmmetConfiguration, getEmmetMode, isStyleSheet, parseDocument, getNode, allowedMimeTypesInScriptTag, trimQuotes, toLSTextDocument } from './util'; -import { getLanguageService, TokenType, Range as LSRange } from 'vscode-html-languageservice'; +import { getEmmetHelper, getMappingForIncludedLanguages, parsePartialStylesheet, getEmmetConfiguration, getEmmetMode, isStyleSheet, getFlatNode, allowedMimeTypesInScriptTag, toLSTextDocument, getHtmlFlatNode } from './util'; +import { Range as LSRange } from 'vscode-languageserver-textdocument'; +import { getRootNode } from './parseDocument'; export class DefaultCompletionItemProvider implements vscode.CompletionItemProvider { private lastCompletionType: string | undefined; - private htmlLS = getLanguageService(); - public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, _: vscode.CancellationToken, context: vscode.CompletionContext): Thenable | undefined { const completionResult = this.provideCompletionItemsInternal(document, position, context); if (!completionResult) { @@ -62,8 +61,8 @@ export class DefaultCompletionItemProvider implements vscode.CompletionItemProvi const helper = getEmmetHelper(); let validateLocation = syntax === 'html' || syntax === 'jsx' || syntax === 'xml'; - let rootNode: Node | undefined = undefined; - let currentNode: Node | null = null; + let rootNode: Node | undefined; + let currentNode: Node | undefined; const lsDoc = toLSTextDocument(document); position = document.validatePosition(position); @@ -81,19 +80,16 @@ export class DefaultCompletionItemProvider implements vscode.CompletionItemProvi default: break; } - } if (validateLocation) { - - const parsedLsDoc = this.htmlLS.parseHTMLDocument(lsDoc); const positionOffset = document.offsetAt(position); - const node = parsedLsDoc.findNodeAt(positionOffset); - - if (node.tag === 'script') { - if (node.attributes && 'type' in node.attributes) { - const rawTypeAttrValue = node.attributes['type']; - if (rawTypeAttrValue) { - const typeAttrValue = trimQuotes(rawTypeAttrValue); + const emmetRootNode = getRootNode(document, true); + const foundNode = getHtmlFlatNode(document.getText(), emmetRootNode, positionOffset, false); + if (foundNode) { + if (foundNode.name === 'script') { + const typeNode = foundNode.attributes.find(attr => attr.name.toString() === 'type'); + if (typeNode) { + const typeAttrValue = typeNode.value.toString(); if (typeAttrValue === 'application/javascript' || typeAttrValue === 'text/javascript') { if (!getSyntaxFromArgs({ language: 'javascript' })) { return; @@ -101,34 +97,19 @@ export class DefaultCompletionItemProvider implements vscode.CompletionItemProvi validateLocation = false; } } - - else if (allowedMimeTypesInScriptTag.indexOf(trimQuotes(rawTypeAttrValue)) > -1) { + else if (allowedMimeTypesInScriptTag.includes(typeAttrValue)) { validateLocation = false; } + } else { + return; } - } else { - return; } - } - else if (node.tag === 'style') { - syntax = 'css'; - validateLocation = false; - } else { - if (node.attributes && node.attributes['style']) { - const scanner = this.htmlLS.createScanner(document.getText(), node.start); - let tokenType = scanner.scan(); - let prevAttr = undefined; - let styleAttrValueRange: [number, number] | undefined = undefined; - while (tokenType !== TokenType.EOS && (scanner.getTokenEnd() <= positionOffset)) { - tokenType = scanner.scan(); - if (tokenType === TokenType.AttributeName) { - prevAttr = scanner.getTokenText(); - } - else if (tokenType === TokenType.AttributeValue && prevAttr === 'style') { - styleAttrValueRange = [scanner.getTokenOffset(), scanner.getTokenEnd()]; - } - } - if (prevAttr === 'style' && styleAttrValueRange && positionOffset > styleAttrValueRange[0] && positionOffset < styleAttrValueRange[1]) { + else if (foundNode.name === 'style') { + syntax = 'css'; + validateLocation = false; + } else { + const styleNode = foundNode.attributes.find(attr => attr.name.toString() === 'style'); + if (styleNode && styleNode.value.start <= positionOffset && positionOffset <= styleNode.value.end) { syntax = 'css'; validateLocation = false; } @@ -145,19 +126,18 @@ export class DefaultCompletionItemProvider implements vscode.CompletionItemProvi return; } + const offset = document.offsetAt(position); if (isStyleSheet(document.languageId) && context.triggerKind !== vscode.CompletionTriggerKind.TriggerForIncompleteCompletions) { validateLocation = true; let usePartialParsing = vscode.workspace.getConfiguration('emmet')['optimizeStylesheetParsing'] === true; - rootNode = usePartialParsing && document.lineCount > 1000 ? parsePartialStylesheet(document, position) : parseDocument(document, false); + rootNode = usePartialParsing && document.lineCount > 1000 ? parsePartialStylesheet(document, position) : getRootNode(document, true); if (!rootNode) { return; } - currentNode = getNode(rootNode, position, true); + currentNode = getFlatNode(rootNode, offset, true); } - - - if (validateLocation && !isValidLocationForEmmetAbbreviation(document, rootNode, currentNode, syntax, position, toRange(extractAbbreviationResults.abbreviationRange))) { + if (validateLocation && !isValidLocationForEmmetAbbreviation(document, rootNode, currentNode, syntax, offset, toRange(extractAbbreviationResults.abbreviationRange))) { return; } diff --git a/extensions/emmet/src/editPoint.ts b/extensions/emmet/src/editPoint.ts index d9151185..239df1f4 100644 --- a/extensions/emmet/src/editPoint.ts +++ b/extensions/emmet/src/editPoint.ts @@ -46,7 +46,7 @@ function findEditPoint(lineNum: number, editor: vscode.TextEditor, position: vsc let line = editor.document.lineAt(lineNum); let lineContent = line.text; - if (lineNum !== position.line && line.isEmptyOrWhitespace) { + if (lineNum !== position.line && line.isEmptyOrWhitespace && lineContent.length) { return new vscode.Selection(lineNum, lineContent.length, lineNum, lineContent.length); } diff --git a/extensions/emmet/src/emmetCommon.ts b/extensions/emmet/src/emmetCommon.ts index e768b03a..d0474286 100644 --- a/extensions/emmet/src/emmetCommon.ts +++ b/extensions/emmet/src/emmetCommon.ts @@ -17,8 +17,9 @@ import { fetchEditPoint } from './editPoint'; import { fetchSelectItem } from './selectItem'; import { evaluateMathExpression } from './evaluateMathExpression'; import { incrementDecrement } from './incrementDecrement'; -import { LANGUAGE_MODES, getMappingForIncludedLanguages, updateEmmetExtensionsPath, getPathBaseName } from './util'; +import { LANGUAGE_MODES, getMappingForIncludedLanguages, updateEmmetExtensionsPath, getPathBaseName, getSyntaxes, getEmmetMode } from './util'; import { reflectCssValue } from './reflectCssValue'; +import { addFileToParseCache, removeFileFromParseCache } from './parseDocument'; export function activateEmmetExtension(context: vscode.ExtensionContext) { registerCompletionProviders(context); @@ -145,6 +146,22 @@ export function activateEmmetExtension(context: vscode.ExtensionContext) { updateEmmetExtensionsPath(true); } })); + + context.subscriptions.push(vscode.workspace.onDidOpenTextDocument((e) => { + const emmetMode = getEmmetMode(e.languageId, []) ?? ''; + const syntaxes = getSyntaxes(); + if (syntaxes.markup.includes(emmetMode) || syntaxes.stylesheet.includes(emmetMode)) { + addFileToParseCache(e); + } + })); + + context.subscriptions.push(vscode.workspace.onDidCloseTextDocument((e) => { + const emmetMode = getEmmetMode(e.languageId, []) ?? ''; + const syntaxes = getSyntaxes(); + if (syntaxes.markup.includes(emmetMode) || syntaxes.stylesheet.includes(emmetMode)) { + removeFileFromParseCache(e); + } + })); } /** diff --git a/extensions/emmet/src/evaluateMathExpression.ts b/extensions/emmet/src/evaluateMathExpression.ts index 588d4dce..cdbcfda1 100644 --- a/extensions/emmet/src/evaluateMathExpression.ts +++ b/extensions/emmet/src/evaluateMathExpression.ts @@ -7,7 +7,6 @@ import * as vscode from 'vscode'; import evaluate, { extract } from '@emmetio/math-expression'; -import { DocumentStreamReader } from './bufferStream'; export function evaluateMathExpression(): Thenable { if (!vscode.window.activeTextEditor) { @@ -15,13 +14,12 @@ export function evaluateMathExpression(): Thenable { return Promise.resolve(false); } const editor = vscode.window.activeTextEditor; - const stream = new DocumentStreamReader(editor.document); return editor.edit(editBuilder => { editor.selections.forEach(selection => { // startpos always comes before endpos const startpos = selection.isReversed ? selection.active : selection.anchor; const endpos = selection.isReversed ? selection.anchor : selection.active; - const selectionText = stream.substring(startpos, endpos); + const selectionText = editor.document.getText(new vscode.Range(startpos, endpos)); try { if (selectionText) { @@ -30,7 +28,7 @@ export function evaluateMathExpression(): Thenable { editBuilder.replace(new vscode.Range(startpos, endpos), result); } else { // no selection made, extract expression from line - const lineToSelectionEnd = stream.substring(new vscode.Position(selection.end.line, 0), endpos); + const lineToSelectionEnd = editor.document.getText(new vscode.Range(new vscode.Position(selection.end.line, 0), endpos)); const extractedIndices = extract(lineToSelectionEnd); if (!extractedIndices) { throw new Error('Invalid extracted indices'); diff --git a/extensions/emmet/src/imageSizeHelper.ts b/extensions/emmet/src/imageSizeHelper.ts index 13ae2239..c761b095 100644 --- a/extensions/emmet/src/imageSizeHelper.ts +++ b/extensions/emmet/src/imageSizeHelper.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ // Based on @sergeche's work on the emmet plugin for atom -// TODO: Move to https://github.com/emmetio/image-size import * as path from 'path'; import * as http from 'http'; diff --git a/extensions/emmet/src/matchTag.ts b/extensions/emmet/src/matchTag.ts index 1e5ead3f..d7331a46 100644 --- a/extensions/emmet/src/matchTag.ts +++ b/extensions/emmet/src/matchTag.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { HtmlNode } from 'EmmetNode'; -import { getHtmlNode, parseDocument, validate } from './util'; - +import { validate, getHtmlFlatNode, offsetRangeToSelection } from './util'; +import { getRootNode } from './parseDocument'; +import { HtmlNode as HtmlFlatNode } from 'EmmetFlatNode'; export function matchTag() { if (!validate(false) || !vscode.window.activeTextEditor) { @@ -14,32 +14,40 @@ export function matchTag() { } const editor = vscode.window.activeTextEditor; - let rootNode: HtmlNode = parseDocument(editor.document); - if (!rootNode) { return; } + const document = editor.document; + const rootNode = getRootNode(document, true); + if (!rootNode) { + return; + } let updatedSelections: vscode.Selection[] = []; editor.selections.forEach(selection => { - let updatedSelection = getUpdatedSelections(editor, selection.start, rootNode); + const updatedSelection = getUpdatedSelections(document, rootNode, selection.start); if (updatedSelection) { updatedSelections.push(updatedSelection); } }); - if (updatedSelections.length > 0) { + if (updatedSelections.length) { editor.selections = updatedSelections; editor.revealRange(editor.selections[updatedSelections.length - 1]); } } -function getUpdatedSelections(editor: vscode.TextEditor, position: vscode.Position, rootNode: HtmlNode): vscode.Selection | undefined { - let currentNode = getHtmlNode(editor.document, rootNode, position, true); - if (!currentNode) { return; } +function getUpdatedSelections(document: vscode.TextDocument, rootNode: HtmlFlatNode, position: vscode.Position): vscode.Selection | undefined { + const offset = document.offsetAt(position); + const currentNode = getHtmlFlatNode(document.getText(), rootNode, offset, true); + if (!currentNode) { + return; + } - // If no closing tag or cursor is between open and close tag, then no-op - if (!currentNode.close || (position.isAfter(currentNode.open.end) && position.isBefore(currentNode.close.start))) { + // If no opening/closing tag or cursor is between open and close tag, then no-op + if (!currentNode.open + || !currentNode.close + || (offset > currentNode.open.end && offset < currentNode.close.start)) { return; } // Place cursor inside the close tag if cursor is inside the open tag, else place it inside the open tag - let finalPosition = position.isBeforeOrEqual(currentNode.open.end) ? currentNode.close.start.translate(0, 2) : currentNode.open.start.translate(0, 1); - return new vscode.Selection(finalPosition, finalPosition); -} \ No newline at end of file + const finalOffset = (offset <= currentNode.open.end) ? currentNode.close.start + 2 : currentNode.start + 1; + return offsetRangeToSelection(document, finalOffset, finalOffset); +} diff --git a/extensions/emmet/src/mergeLines.ts b/extensions/emmet/src/mergeLines.ts index cebcea30..ef2f37f7 100644 --- a/extensions/emmet/src/mergeLines.ts +++ b/extensions/emmet/src/mergeLines.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { Node } from 'EmmetNode'; -import { getNode, parseDocument, validate } from './util'; +import { Node } from 'EmmetFlatNode'; +import { getFlatNode, offsetRangeToVsRange, validate } from './util'; +import { getRootNode } from './parseDocument'; export function mergeLines() { if (!validate(false) || !vscode.window.activeTextEditor) { @@ -14,14 +15,14 @@ export function mergeLines() { const editor = vscode.window.activeTextEditor; - let rootNode = parseDocument(editor.document); + const rootNode = getRootNode(editor.document, true); if (!rootNode) { return; } return editor.edit(editBuilder => { editor.selections.reverse().forEach(selection => { - let textEdit = getRangesToReplace(editor.document, selection, rootNode!); + const textEdit = getRangesToReplace(editor.document, selection, rootNode); if (textEdit) { editBuilder.replace(textEdit.range, textEdit.newText); } @@ -30,25 +31,36 @@ export function mergeLines() { } function getRangesToReplace(document: vscode.TextDocument, selection: vscode.Selection, rootNode: Node): vscode.TextEdit | undefined { - let startNodeToUpdate: Node | null; - let endNodeToUpdate: Node | null; + let startNodeToUpdate: Node | undefined; + let endNodeToUpdate: Node | undefined; + const selectionStart = document.offsetAt(selection.start); + const selectionEnd = document.offsetAt(selection.end); if (selection.isEmpty) { - startNodeToUpdate = endNodeToUpdate = getNode(rootNode, selection.start, true); + startNodeToUpdate = endNodeToUpdate = getFlatNode(rootNode, selectionStart, true); } else { - startNodeToUpdate = getNode(rootNode, selection.start, true); - endNodeToUpdate = getNode(rootNode, selection.end, true); + startNodeToUpdate = getFlatNode(rootNode, selectionStart, true); + endNodeToUpdate = getFlatNode(rootNode, selectionEnd, true); } - if (!startNodeToUpdate || !endNodeToUpdate || startNodeToUpdate.start.line === endNodeToUpdate.end.line) { + if (!startNodeToUpdate || !endNodeToUpdate) { return; } - let rangeToReplace = new vscode.Range(startNodeToUpdate.start, endNodeToUpdate.end); - let textToReplaceWith = document.lineAt(startNodeToUpdate.start.line).text.substr(startNodeToUpdate.start.character); - for (let i = startNodeToUpdate.start.line + 1; i <= endNodeToUpdate.end.line; i++) { + const startPos = document.positionAt(startNodeToUpdate.start); + const startLine = startPos.line; + const startChar = startPos.character; + const endPos = document.positionAt(endNodeToUpdate.end); + const endLine = endPos.line; + if (startLine === endLine) { + return; + } + + const rangeToReplace = offsetRangeToVsRange(document, startNodeToUpdate.start, endNodeToUpdate.end); + let textToReplaceWith = document.lineAt(startLine).text.substr(startChar); + for (let i = startLine + 1; i <= endLine; i++) { textToReplaceWith += document.lineAt(i).text.trim(); } return new vscode.TextEdit(rangeToReplace, textToReplaceWith); -} \ No newline at end of file +} diff --git a/extensions/emmet/src/parseDocument.ts b/extensions/emmet/src/parseDocument.ts new file mode 100644 index 00000000..c062e9d8 --- /dev/null +++ b/extensions/emmet/src/parseDocument.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TextDocument } from 'vscode'; +import { Node as FlatNode } from 'EmmetFlatNode'; +import parse from '@emmetio/html-matcher'; +import parseStylesheet from '@emmetio/css-parser'; +import { isStyleSheet } from './util'; + +type Pair = { + key: K; + value: V; +}; + +// Map(filename, Pair(fileVersion, rootNodeOfParsedContent)) +const _parseCache = new Map | undefined>(); + +export function getRootNode(document: TextDocument, useCache: boolean): FlatNode { + const key = document.uri.toString(); + const result = _parseCache.get(key); + const documentVersion = document.version; + if (useCache && result) { + if (documentVersion === result.key) { + return result.value; + } + } + + const parseContent = isStyleSheet(document.languageId) ? parseStylesheet : parse; + const rootNode = parseContent(document.getText()); + if (useCache) { + _parseCache.set(key, { key: documentVersion, value: rootNode }); + } + return rootNode; +} + +export function addFileToParseCache(document: TextDocument) { + const filename = document.uri.toString(); + _parseCache.set(filename, undefined); +} + +export function removeFileFromParseCache(document: TextDocument) { + const filename = document.uri.toString(); + _parseCache.delete(filename); +} diff --git a/extensions/emmet/src/reflectCssValue.ts b/extensions/emmet/src/reflectCssValue.ts index da20b383..6992e629 100644 --- a/extensions/emmet/src/reflectCssValue.ts +++ b/extensions/emmet/src/reflectCssValue.ts @@ -3,20 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Range, window, TextEditor } from 'vscode'; -import { getCssPropertyFromRule, getCssPropertyFromDocument } from './util'; -import { Property, Rule } from 'EmmetNode'; +import { window, TextEditor } from 'vscode'; +import { getCssPropertyFromRule, getCssPropertyFromDocument, offsetRangeToVsRange } from './util'; +import { Property, Rule } from 'EmmetFlatNode'; const vendorPrefixes = ['-webkit-', '-moz-', '-ms-', '-o-', '']; export function reflectCssValue(): Thenable | undefined { - let editor = window.activeTextEditor; + const editor = window.activeTextEditor; if (!editor) { window.showInformationMessage('No editor is active.'); return; } - let node = getCssPropertyFromDocument(editor, editor.selection.active); + const node = getCssPropertyFromDocument(editor, editor.selection.active); if (!node) { return; } @@ -45,10 +45,11 @@ function updateCSSNode(editor: TextEditor, property: Property): ThenableparseDocument(editor.document); + const document = editor.document; + const rootNode = getRootNode(document, true); if (!rootNode) { return; } - let indentInSpaces = ''; - const tabSize: number = editor.options.tabSize ? +editor.options.tabSize : 0; - for (let i = 0; i < tabSize || 0; i++) { - indentInSpaces += ' '; - } - - let rangesToRemove: vscode.Range[] = []; - editor.selections.reverse().forEach(selection => { - rangesToRemove = rangesToRemove.concat(getRangeToRemove(editor, rootNode, selection, indentInSpaces)); - }); + let finalRangesToRemove = editor.selections.reverse() + .reduce((prev, selection) => + prev.concat(getRangesToRemove(editor.document, rootNode, selection)), []); return editor.edit(editBuilder => { - rangesToRemove.forEach(range => { + finalRangesToRemove.forEach(range => { editBuilder.replace(range, ''); }); }); } -function getRangeToRemove(editor: vscode.TextEditor, rootNode: HtmlNode, selection: vscode.Selection, indentInSpaces: string): vscode.Range[] { - - let nodeToUpdate = getHtmlNode(editor.document, rootNode, selection.start, true); +/** + * Calculates the ranges to remove, along with what to replace those ranges with. + * It finds the node to remove based on the selection's start position + * and then removes that node, reindenting the content in between. + */ +function getRangesToRemove(document: vscode.TextDocument, rootNode: HtmlFlatNode, selection: vscode.Selection): vscode.Range[] { + const offset = document.offsetAt(selection.start); + const nodeToUpdate = getHtmlFlatNode(document.getText(), rootNode, offset, true); if (!nodeToUpdate) { return []; } - let openRange = new vscode.Range(nodeToUpdate.open.start, nodeToUpdate.open.end); - let closeRange: vscode.Range | null = null; + let openTagRange: vscode.Range | undefined; + if (nodeToUpdate.open) { + openTagRange = offsetRangeToVsRange(document, nodeToUpdate.open.start, nodeToUpdate.open.end); + } + let closeTagRange: vscode.Range | undefined; if (nodeToUpdate.close) { - closeRange = new vscode.Range(nodeToUpdate.close.start, nodeToUpdate.close.end); + closeTagRange = offsetRangeToVsRange(document, nodeToUpdate.close.start, nodeToUpdate.close.end); } - let ranges = [openRange]; - if (closeRange) { - for (let i = openRange.start.line + 1; i <= closeRange.start.line; i++) { - let lineContent = editor.document.lineAt(i).text; - if (lineContent.startsWith('\t')) { - ranges.push(new vscode.Range(i, 0, i, 1)); - } else if (lineContent.startsWith(indentInSpaces)) { - ranges.push(new vscode.Range(i, 0, i, indentInSpaces.length)); + let rangesToRemove = []; + if (openTagRange) { + rangesToRemove.push(openTagRange); + if (closeTagRange) { + const indentAmountToRemove = calculateIndentAmountToRemove(document, openTagRange, closeTagRange); + for (let i = openTagRange.start.line + 1; i < closeTagRange.start.line; i++) { + rangesToRemove.push(new vscode.Range(i, 0, i, indentAmountToRemove)); } + rangesToRemove.push(closeTagRange); } - ranges.push(closeRange); } - return ranges; + return rangesToRemove; } +/** + * Calculates the amount of indent to remove for getRangesToRemove. + */ +function calculateIndentAmountToRemove(document: vscode.TextDocument, openRange: vscode.Range, closeRange: vscode.Range): number { + const startLine = openRange.start.line; + const endLine = closeRange.start.line; + + const startLineIndent = document.lineAt(startLine).firstNonWhitespaceCharacterIndex; + const endLineIndent = document.lineAt(endLine).firstNonWhitespaceCharacterIndex; + + let contentIndent: number | undefined; + for (let i = startLine + 1; i < endLine; i++) { + const lineIndent = document.lineAt(i).firstNonWhitespaceCharacterIndex; + contentIndent = !contentIndent ? lineIndent : Math.min(contentIndent, lineIndent); + } + + let indentAmount = 0; + if (contentIndent) { + if (contentIndent < startLineIndent || contentIndent < endLineIndent) { + indentAmount = 0; + } + else { + indentAmount = Math.min(contentIndent - startLineIndent, contentIndent - endLineIndent); + } + } + return indentAmount; +} diff --git a/extensions/emmet/src/selectItem.ts b/extensions/emmet/src/selectItem.ts index 9fa8a5a4..52e672ed 100644 --- a/extensions/emmet/src/selectItem.ts +++ b/extensions/emmet/src/selectItem.ts @@ -4,17 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { validate, parseDocument, isStyleSheet } from './util'; +import { validate, isStyleSheet } from './util'; import { nextItemHTML, prevItemHTML } from './selectItemHTML'; import { nextItemStylesheet, prevItemStylesheet } from './selectItemStylesheet'; -import { HtmlNode, CssNode } from 'EmmetNode'; +import { HtmlNode, CssNode } from 'EmmetFlatNode'; +import { getRootNode } from './parseDocument'; export function fetchSelectItem(direction: string): void { if (!validate() || !vscode.window.activeTextEditor) { return; } const editor = vscode.window.activeTextEditor; - let rootNode = parseDocument(editor.document); + const document = editor.document; + const rootNode = getRootNode(document, true); if (!rootNode) { return; } @@ -26,12 +28,16 @@ export function fetchSelectItem(direction: string): void { let updatedSelection; if (isStyleSheet(editor.document.languageId)) { - updatedSelection = direction === 'next' ? nextItemStylesheet(selectionStart, selectionEnd, rootNode!) : prevItemStylesheet(selectionStart, selectionEnd, rootNode!); + updatedSelection = direction === 'next' ? + nextItemStylesheet(document, selectionStart, selectionEnd, rootNode) : + prevItemStylesheet(document, selectionStart, selectionEnd, rootNode); } else { - updatedSelection = direction === 'next' ? nextItemHTML(selectionStart, selectionEnd, editor, rootNode!) : prevItemHTML(selectionStart, selectionEnd, editor, rootNode!); + updatedSelection = direction === 'next' ? + nextItemHTML(document, selectionStart, selectionEnd, rootNode) : + prevItemHTML(document, selectionStart, selectionEnd, rootNode); } newSelections.push(updatedSelection ? updatedSelection : selection); }); editor.selections = newSelections; editor.revealRange(editor.selections[editor.selections.length - 1]); -} \ No newline at end of file +} diff --git a/extensions/emmet/src/selectItemHTML.ts b/extensions/emmet/src/selectItemHTML.ts index 4af29cd7..f818f578 100644 --- a/extensions/emmet/src/selectItemHTML.ts +++ b/extensions/emmet/src/selectItemHTML.ts @@ -4,11 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { getDeepestNode, findNextWord, findPrevWord, getHtmlNode, isNumber } from './util'; -import { HtmlNode } from 'EmmetNode'; +import { getDeepestFlatNode, findNextWord, findPrevWord, getHtmlFlatNode, offsetRangeToSelection } from './util'; +import { HtmlNode } from 'EmmetFlatNode'; -export function nextItemHTML(selectionStart: vscode.Position, selectionEnd: vscode.Position, editor: vscode.TextEditor, rootNode: HtmlNode): vscode.Selection | undefined { - let currentNode = getHtmlNode(editor.document, rootNode, selectionEnd, false); +export function nextItemHTML(document: vscode.TextDocument, selectionStart: vscode.Position, selectionEnd: vscode.Position, rootNode: HtmlNode): vscode.Selection | undefined { + const selectionEndOffset = document.offsetAt(selectionEnd); + let currentNode = getHtmlFlatNode(document.getText(), rootNode, selectionEndOffset, false); let nextNode: HtmlNode | undefined = undefined; if (!currentNode) { @@ -17,13 +18,16 @@ export function nextItemHTML(selectionStart: vscode.Position, selectionEnd: vsco if (currentNode.type !== 'comment') { // If cursor is in the tag name, select tag - if (selectionEnd.isBefore(currentNode.open.start.translate(0, currentNode.name.length))) { - return getSelectionFromNode(currentNode); + if (currentNode.open && + selectionEndOffset < currentNode.open.start + currentNode.name.length) { + return getSelectionFromNode(document, currentNode); } // If cursor is in the open tag, look for attributes - if (selectionEnd.isBefore(currentNode.open.end)) { - let attrSelection = getNextAttribute(selectionStart, selectionEnd, currentNode); + if (currentNode.open && + selectionEndOffset < currentNode.open.end) { + const selectionStartOffset = document.offsetAt(selectionStart); + const attrSelection = getNextAttribute(document, selectionStartOffset, selectionEndOffset, currentNode); if (attrSelection) { return attrSelection; } @@ -31,12 +35,11 @@ export function nextItemHTML(selectionStart: vscode.Position, selectionEnd: vsco // Get the first child of current node which is right after the cursor and is not a comment nextNode = currentNode.firstChild; - while (nextNode && (selectionEnd.isAfterOrEqual(nextNode.end) || nextNode.type === 'comment')) { + while (nextNode && (selectionEndOffset >= nextNode.end || nextNode.type === 'comment')) { nextNode = nextNode.nextSibling; } } - // Get next sibling of current node which is not a comment. If none is found try the same on the parent while (!nextNode && currentNode) { if (currentNode.nextSibling) { @@ -50,33 +53,36 @@ export function nextItemHTML(selectionStart: vscode.Position, selectionEnd: vsco } } - return nextNode && getSelectionFromNode(nextNode); + return nextNode && getSelectionFromNode(document, nextNode); } -export function prevItemHTML(selectionStart: vscode.Position, selectionEnd: vscode.Position, editor: vscode.TextEditor, rootNode: HtmlNode): vscode.Selection | undefined { - let currentNode = getHtmlNode(editor.document, rootNode, selectionStart, false); +export function prevItemHTML(document: vscode.TextDocument, selectionStart: vscode.Position, selectionEnd: vscode.Position, rootNode: HtmlNode): vscode.Selection | undefined { + const selectionStartOffset = document.offsetAt(selectionStart); + let currentNode = getHtmlFlatNode(document.getText(), rootNode, selectionStartOffset, false); let prevNode: HtmlNode | undefined = undefined; if (!currentNode) { return; } - if (currentNode.type !== 'comment' && selectionStart.translate(0, -1).isAfter(currentNode.open.start)) { - - if (selectionStart.isBefore(currentNode.open.end) || !currentNode.firstChild || selectionEnd.isBeforeOrEqual(currentNode.firstChild.start)) { + const selectionEndOffset = document.offsetAt(selectionEnd); + if (currentNode.open && + currentNode.type !== 'comment' && + selectionStartOffset - 1 > currentNode.open.start) { + if (selectionStartOffset < currentNode.open.end || !currentNode.firstChild || selectionEndOffset <= currentNode.firstChild.start) { prevNode = currentNode; } else { // Select the child that appears just before the cursor and is not a comment prevNode = currentNode.firstChild; let oldOption: HtmlNode | undefined = undefined; - while (prevNode.nextSibling && selectionStart.isAfterOrEqual(prevNode.nextSibling.end)) { + while (prevNode.nextSibling && selectionStartOffset >= prevNode.nextSibling.end) { if (prevNode && prevNode.type !== 'comment') { oldOption = prevNode; } prevNode = prevNode.nextSibling; } - prevNode = getDeepestNode((prevNode && prevNode.type !== 'comment') ? prevNode : oldOption); + prevNode = getDeepestFlatNode((prevNode && prevNode.type !== 'comment') ? prevNode : oldOption); } } @@ -84,7 +90,7 @@ export function prevItemHTML(selectionStart: vscode.Position, selectionEnd: vsco while (!prevNode && currentNode) { if (currentNode.previousSibling) { if (currentNode.previousSibling.type !== 'comment') { - prevNode = getDeepestNode(currentNode.previousSibling); + prevNode = getDeepestFlatNode(currentNode.previousSibling); } else { currentNode = currentNode.previousSibling; } @@ -98,66 +104,66 @@ export function prevItemHTML(selectionStart: vscode.Position, selectionEnd: vsco return undefined; } - let attrSelection = getPrevAttribute(selectionStart, selectionEnd, prevNode); - return attrSelection ? attrSelection : getSelectionFromNode(prevNode); + const attrSelection = getPrevAttribute(document, selectionStartOffset, selectionEndOffset, prevNode); + return attrSelection ? attrSelection : getSelectionFromNode(document, prevNode); } -function getSelectionFromNode(node: HtmlNode): vscode.Selection | undefined { +function getSelectionFromNode(document: vscode.TextDocument, node: HtmlNode): vscode.Selection | undefined { if (node && node.open) { - let selectionStart = (node.open.start).translate(0, 1); - let selectionEnd = selectionStart.translate(0, node.name.length); - - return new vscode.Selection(selectionStart, selectionEnd); + const selectionStart = node.open.start + 1; + const selectionEnd = selectionStart + node.name.length; + return offsetRangeToSelection(document, selectionStart, selectionEnd); } return undefined; } -function getNextAttribute(selectionStart: vscode.Position, selectionEnd: vscode.Position, node: HtmlNode): vscode.Selection | undefined { - +function getNextAttribute(document: vscode.TextDocument, selectionStart: number, selectionEnd: number, node: HtmlNode): vscode.Selection | undefined { if (!node.attributes || node.attributes.length === 0 || node.type === 'comment') { return; } for (const attr of node.attributes) { - if (selectionEnd.isBefore(attr.start)) { + if (selectionEnd < attr.start) { // select full attr - return new vscode.Selection(attr.start, attr.end); + return offsetRangeToSelection(document, attr.start, attr.end); } - if (!attr.value || (attr.value.start).isEqual(attr.value.end)) { + if (!attr.value || attr.value.start === attr.value.end) { // No attr value to select continue; } - if ((selectionStart.isEqual(attr.start) && selectionEnd.isEqual(attr.end)) || selectionEnd.isBefore(attr.value.start)) { + if ((selectionStart === attr.start && selectionEnd === attr.end) || + selectionEnd < attr.value.start) { // cursor is in attr name, so select full attr value - return new vscode.Selection(attr.value.start, attr.value.end); + return offsetRangeToSelection(document, attr.value.start, attr.value.end); } // Fetch the next word in the attr value - if (attr.value.toString().indexOf(' ') === -1) { // attr value does not have space, so no next word to find continue; } let pos: number | undefined = undefined; - if (selectionStart.isEqual(attr.value.start) && selectionEnd.isEqual(attr.value.end)) { + if (selectionStart === attr.value.start && selectionEnd === attr.value.end) { pos = -1; } - if (pos === undefined && selectionEnd.isBefore(attr.end)) { - pos = selectionEnd.character - attr.value.start.character - 1; + if (pos === undefined && selectionEnd < attr.end) { + const selectionEndCharacter = document.positionAt(selectionEnd).character; + const attrValueStartCharacter = document.positionAt(attr.value.start).character; + pos = selectionEndCharacter - attrValueStartCharacter - 1; } if (pos !== undefined) { - let [newSelectionStartOffset, newSelectionEndOffset] = findNextWord(attr.value.toString(), pos); - if (!isNumber(newSelectionStartOffset) || !isNumber(newSelectionEndOffset)) { + const [newSelectionStartOffset, newSelectionEndOffset] = findNextWord(attr.value.toString(), pos); + if (newSelectionStartOffset === undefined || newSelectionEndOffset === undefined) { return; } if (newSelectionStartOffset >= 0 && newSelectionEndOffset >= 0) { - const newSelectionStart = (attr.value.start).translate(0, newSelectionStartOffset); - const newSelectionEnd = (attr.value.start).translate(0, newSelectionEndOffset); - return new vscode.Selection(newSelectionStart, newSelectionEnd); + const newSelectionStart = attr.value.start + newSelectionStartOffset; + const newSelectionEnd = attr.value.start + newSelectionEndOffset; + return offsetRangeToSelection(document, newSelectionStart, newSelectionEnd); } } @@ -166,44 +172,44 @@ function getNextAttribute(selectionStart: vscode.Position, selectionEnd: vscode. return; } -function getPrevAttribute(selectionStart: vscode.Position, selectionEnd: vscode.Position, node: HtmlNode): vscode.Selection | undefined { - +function getPrevAttribute(document: vscode.TextDocument, selectionStart: number, selectionEnd: number, node: HtmlNode): vscode.Selection | undefined { if (!node.attributes || node.attributes.length === 0 || node.type === 'comment') { return; } for (let i = node.attributes.length - 1; i >= 0; i--) { - let attr = node.attributes[i]; - - if (selectionStart.isBeforeOrEqual(attr.start)) { + const attr = node.attributes[i]; + if (selectionStart <= attr.start) { continue; } - if (!attr.value || (attr.value.start).isEqual(attr.value.end) || selectionStart.isBefore(attr.value.start)) { + if (!attr.value || attr.value.start === attr.value.end || selectionStart < attr.value.start) { // select full attr - return new vscode.Selection(attr.start, attr.end); + return offsetRangeToSelection(document, attr.start, attr.end); } - if (selectionStart.isEqual(attr.value.start)) { - if (selectionEnd.isAfterOrEqual(attr.value.end)) { + if (selectionStart === attr.value.start) { + if (selectionEnd >= attr.value.end) { // select full attr - return new vscode.Selection(attr.start, attr.end); + return offsetRangeToSelection(document, attr.start, attr.end); } // select attr value - return new vscode.Selection(attr.value.start, attr.value.end); + return offsetRangeToSelection(document, attr.value.start, attr.value.end); } // Fetch the prev word in the attr value - - let pos = selectionStart.isAfter(attr.value.end) ? attr.value.toString().length : selectionStart.character - attr.value.start.character; - let [newSelectionStartOffset, newSelectionEndOffset] = findPrevWord(attr.value.toString(), pos); - if (!isNumber(newSelectionStartOffset) || !isNumber(newSelectionEndOffset)) { + const selectionStartCharacter = document.positionAt(selectionStart).character; + const attrValueStartCharacter = document.positionAt(attr.value.start).character; + const pos = selectionStart > attr.value.end ? attr.value.toString().length : + selectionStartCharacter - attrValueStartCharacter; + const [newSelectionStartOffset, newSelectionEndOffset] = findPrevWord(attr.value.toString(), pos); + if (newSelectionStartOffset === undefined || newSelectionEndOffset === undefined) { return; } if (newSelectionStartOffset >= 0 && newSelectionEndOffset >= 0) { - const newSelectionStart = (attr.value.start).translate(0, newSelectionStartOffset); - const newSelectionEnd = (attr.value.start).translate(0, newSelectionEndOffset); - return new vscode.Selection(newSelectionStart, newSelectionEnd); + const newSelectionStart = attr.value.start + newSelectionStartOffset; + const newSelectionEnd = attr.value.start + newSelectionEndOffset; + return offsetRangeToSelection(document, newSelectionStart, newSelectionEnd); } } diff --git a/extensions/emmet/src/selectItemStylesheet.ts b/extensions/emmet/src/selectItemStylesheet.ts index 420b4c15..557d9713 100644 --- a/extensions/emmet/src/selectItemStylesheet.ts +++ b/extensions/emmet/src/selectItemStylesheet.ts @@ -4,11 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { getDeepestNode, findNextWord, findPrevWord, getNode } from './util'; -import { Node, CssNode, Rule, Property } from 'EmmetNode'; +import { getDeepestFlatNode, findNextWord, findPrevWord, getFlatNode, offsetRangeToSelection } from './util'; +import { Node, CssNode, Rule, Property } from 'EmmetFlatNode'; -export function nextItemStylesheet(startOffset: vscode.Position, endOffset: vscode.Position, rootNode: Node): vscode.Selection | undefined { - let currentNode = getNode(rootNode, endOffset, true); +export function nextItemStylesheet(document: vscode.TextDocument, startPosition: vscode.Position, endPosition: vscode.Position, rootNode: Node): vscode.Selection | undefined { + const startOffset = document.offsetAt(startPosition); + const endOffset = document.offsetAt(endPosition); + let currentNode: CssNode | undefined = getFlatNode(rootNode, endOffset, true); if (!currentNode) { currentNode = rootNode; } @@ -16,27 +18,31 @@ export function nextItemStylesheet(startOffset: vscode.Position, endOffset: vsco return; } // Full property is selected, so select full property value next - if (currentNode.type === 'property' && startOffset.isEqual(currentNode.start) && endOffset.isEqual(currentNode.end)) { - return getSelectionFromProperty(currentNode, startOffset, endOffset, true, 'next'); + if (currentNode.type === 'property' && + startOffset === currentNode.start && + endOffset === currentNode.end) { + return getSelectionFromProperty(document, currentNode, startOffset, endOffset, true, 'next'); } // Part or whole of propertyValue is selected, so select the next word in the propertyValue - if (currentNode.type === 'property' && startOffset.isAfterOrEqual((currentNode).valueToken.start) && endOffset.isBeforeOrEqual((currentNode).valueToken.end)) { - let singlePropertyValue = getSelectionFromProperty(currentNode, startOffset, endOffset, false, 'next'); + if (currentNode.type === 'property' && + startOffset >= (currentNode).valueToken.start && + endOffset <= (currentNode).valueToken.end) { + let singlePropertyValue = getSelectionFromProperty(document, currentNode, startOffset, endOffset, false, 'next'); if (singlePropertyValue) { return singlePropertyValue; } } // Cursor is in the selector or in a property - if ((currentNode.type === 'rule' && endOffset.isBefore((currentNode).selectorToken.end)) - || (currentNode.type === 'property' && endOffset.isBefore((currentNode).valueToken.end))) { - return getSelectionFromNode(currentNode); + if ((currentNode.type === 'rule' && endOffset < (currentNode).selectorToken.end) + || (currentNode.type === 'property' && endOffset < (currentNode).valueToken.end)) { + return getSelectionFromNode(document, currentNode); } // Get the first child of current node which is right after the cursor let nextNode = currentNode.firstChild; - while (nextNode && endOffset.isAfterOrEqual(nextNode.end)) { + while (nextNode && endOffset >= nextNode.end) { nextNode = nextNode.nextSibling; } @@ -46,12 +52,13 @@ export function nextItemStylesheet(startOffset: vscode.Position, endOffset: vsco currentNode = currentNode.parent; } - return getSelectionFromNode(nextNode); - + return nextNode ? getSelectionFromNode(document, nextNode) : undefined; } -export function prevItemStylesheet(startOffset: vscode.Position, endOffset: vscode.Position, rootNode: CssNode): vscode.Selection | undefined { - let currentNode = getNode(rootNode, startOffset, false); +export function prevItemStylesheet(document: vscode.TextDocument, startPosition: vscode.Position, endPosition: vscode.Position, rootNode: CssNode): vscode.Selection | undefined { + const startOffset = document.offsetAt(startPosition); + const endOffset = document.offsetAt(endPosition); + let currentNode = getFlatNode(rootNode, startOffset, false); if (!currentNode) { currentNode = rootNode; } @@ -60,70 +67,80 @@ export function prevItemStylesheet(startOffset: vscode.Position, endOffset: vsco } // Full property value is selected, so select the whole property next - if (currentNode.type === 'property' && startOffset.isEqual((currentNode).valueToken.start) && endOffset.isEqual((currentNode).valueToken.end)) { - return getSelectionFromNode(currentNode); + if (currentNode.type === 'property' && + startOffset === (currentNode).valueToken.start && + endOffset === (currentNode).valueToken.end) { + return getSelectionFromNode(document, currentNode); } // Part of propertyValue is selected, so select the prev word in the propertyValue - if (currentNode.type === 'property' && startOffset.isAfterOrEqual((currentNode).valueToken.start) && endOffset.isBeforeOrEqual((currentNode).valueToken.end)) { - let singlePropertyValue = getSelectionFromProperty(currentNode, startOffset, endOffset, false, 'prev'); + if (currentNode.type === 'property' && + startOffset >= (currentNode).valueToken.start && + endOffset <= (currentNode).valueToken.end) { + let singlePropertyValue = getSelectionFromProperty(document, currentNode, startOffset, endOffset, false, 'prev'); if (singlePropertyValue) { return singlePropertyValue; } } - if (currentNode.type === 'property' || !currentNode.firstChild || (currentNode.type === 'rule' && startOffset.isBeforeOrEqual(currentNode.firstChild.start))) { - return getSelectionFromNode(currentNode); + if (currentNode.type === 'property' || !currentNode.firstChild || + (currentNode.type === 'rule' && startOffset <= currentNode.firstChild.start)) { + return getSelectionFromNode(document, currentNode); } // Select the child that appears just before the cursor - let prevNode = currentNode.firstChild; - while (prevNode.nextSibling && startOffset.isAfterOrEqual(prevNode.nextSibling.end)) { + let prevNode: CssNode | undefined = currentNode.firstChild; + while (prevNode.nextSibling && startOffset >= prevNode.nextSibling.end) { prevNode = prevNode.nextSibling; } - prevNode = getDeepestNode(prevNode); - - return getSelectionFromProperty(prevNode, startOffset, endOffset, false, 'prev'); + prevNode = getDeepestFlatNode(prevNode); + return getSelectionFromProperty(document, prevNode, startOffset, endOffset, false, 'prev'); } -function getSelectionFromNode(node: Node): vscode.Selection | undefined { +function getSelectionFromNode(document: vscode.TextDocument, node: Node | undefined): vscode.Selection | undefined { if (!node) { return; } - let nodeToSelect = node.type === 'rule' ? (node).selectorToken : node; - return new vscode.Selection(nodeToSelect.start, nodeToSelect.end); + const nodeToSelect = node.type === 'rule' ? (node).selectorToken : node; + return offsetRangeToSelection(document, nodeToSelect.start, nodeToSelect.end); } -function getSelectionFromProperty(node: Node, selectionStart: vscode.Position, selectionEnd: vscode.Position, selectFullValue: boolean, direction: string): vscode.Selection | undefined { +function getSelectionFromProperty(document: vscode.TextDocument, node: Node | undefined, selectionStart: number, selectionEnd: number, selectFullValue: boolean, direction: string): vscode.Selection | undefined { if (!node || node.type !== 'property') { return; } const propertyNode = node; let propertyValue = propertyNode.valueToken.stream.substring(propertyNode.valueToken.start, propertyNode.valueToken.end); - selectFullValue = selectFullValue || (direction === 'prev' && selectionStart.isEqual(propertyNode.valueToken.start) && selectionEnd.isBefore(propertyNode.valueToken.end)); + selectFullValue = selectFullValue || + (direction === 'prev' && selectionStart === propertyNode.valueToken.start && selectionEnd < propertyNode.valueToken.end); if (selectFullValue) { - return new vscode.Selection(propertyNode.valueToken.start, propertyNode.valueToken.end); + return offsetRangeToSelection(document, propertyNode.valueToken.start, propertyNode.valueToken.end); } let pos: number = -1; if (direction === 'prev') { - if (selectionStart.isEqual(propertyNode.valueToken.start)) { + if (selectionStart === propertyNode.valueToken.start) { return; } - pos = selectionStart.isAfter(propertyNode.valueToken.end) ? propertyValue.length : selectionStart.character - propertyNode.valueToken.start.character; - } - - if (direction === 'next') { - if (selectionEnd.isEqual(propertyNode.valueToken.end) && (selectionStart.isAfter(propertyNode.valueToken.start) || propertyValue.indexOf(' ') === -1)) { + const selectionStartChar = document.positionAt(selectionStart).character; + const tokenStartChar = document.positionAt(propertyNode.valueToken.start).character; + pos = selectionStart > propertyNode.valueToken.end ? propertyValue.length : + selectionStartChar - tokenStartChar; + } else if (direction === 'next') { + if (selectionEnd === propertyNode.valueToken.end && + (selectionStart > propertyNode.valueToken.start || !propertyValue.includes(' '))) { return; } - pos = selectionEnd.isEqual(propertyNode.valueToken.end) ? -1 : selectionEnd.character - propertyNode.valueToken.start.character - 1; + const selectionEndChar = document.positionAt(selectionEnd).character; + const tokenStartChar = document.positionAt(propertyNode.valueToken.start).character; + pos = selectionEnd === propertyNode.valueToken.end ? -1 : + selectionEndChar - tokenStartChar - 1; } @@ -132,8 +149,9 @@ function getSelectionFromProperty(node: Node, selectionStart: vscode.Position, s return; } - const newSelectionStart = (propertyNode.valueToken.start).translate(0, newSelectionStartOffset); - const newSelectionEnd = (propertyNode.valueToken.start).translate(0, newSelectionEndOffset); + const tokenStart = document.positionAt(propertyNode.valueToken.start); + const newSelectionStart = tokenStart.translate(0, newSelectionStartOffset); + const newSelectionEnd = tokenStart.translate(0, newSelectionEndOffset); return new vscode.Selection(newSelectionStart, newSelectionEnd); } diff --git a/extensions/emmet/src/splitJoinTag.ts b/extensions/emmet/src/splitJoinTag.ts index a5f1d255..3d1df021 100644 --- a/extensions/emmet/src/splitJoinTag.ts +++ b/extensions/emmet/src/splitJoinTag.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { HtmlNode } from 'EmmetNode'; -import { getHtmlNode, parseDocument, validate, getEmmetMode, getEmmetConfiguration } from './util'; +import { validate, getEmmetMode, getEmmetConfiguration, getHtmlFlatNode, offsetRangeToVsRange } from './util'; +import { HtmlNode as HtmlFlatNode } from 'EmmetFlatNode'; +import { getRootNode } from './parseDocument'; export function splitJoinTag() { if (!validate(false) || !vscode.window.activeTextEditor) { @@ -13,40 +14,43 @@ export function splitJoinTag() { } const editor = vscode.window.activeTextEditor; - let rootNode = parseDocument(editor.document); + const document = editor.document; + const rootNode = getRootNode(editor.document, true); if (!rootNode) { return; } return editor.edit(editBuilder => { editor.selections.reverse().forEach(selection => { - let nodeToUpdate = getHtmlNode(editor.document, rootNode, selection.start, true); + const documentText = document.getText(); + const offset = document.offsetAt(selection.start); + const nodeToUpdate = getHtmlFlatNode(documentText, rootNode, offset, true); if (nodeToUpdate) { - let textEdit = getRangesToReplace(editor.document, nodeToUpdate); + const textEdit = getRangesToReplace(document, nodeToUpdate); editBuilder.replace(textEdit.range, textEdit.newText); } }); }); } -function getRangesToReplace(document: vscode.TextDocument, nodeToUpdate: HtmlNode): vscode.TextEdit { +function getRangesToReplace(document: vscode.TextDocument, nodeToUpdate: HtmlFlatNode): vscode.TextEdit { let rangeToReplace: vscode.Range; let textToReplaceWith: string; - if (!nodeToUpdate.close) { + if (!nodeToUpdate.open || !nodeToUpdate.close) { // Split Tag - let nodeText = document.getText(new vscode.Range(nodeToUpdate.start, nodeToUpdate.end)); - let m = nodeText.match(/(\s*\/)?>$/); - let end = nodeToUpdate.end; - let start = m ? end.translate(0, -m[0].length) : end; + const nodeText = document.getText().substring(nodeToUpdate.start, nodeToUpdate.end); + const m = nodeText.match(/(\s*\/)?>$/); + const end = nodeToUpdate.end; + const start = m ? end - m[0].length : end; - rangeToReplace = new vscode.Range(start, end); + rangeToReplace = offsetRangeToVsRange(document, start, end); textToReplaceWith = `>`; } else { // Join Tag - let start = (nodeToUpdate.open.end).translate(0, -1); - let end = nodeToUpdate.end; - rangeToReplace = new vscode.Range(start, end); + const start = nodeToUpdate.open.end - 1; + const end = nodeToUpdate.end; + rangeToReplace = offsetRangeToVsRange(document, start, end); textToReplaceWith = '/>'; const emmetMode = getEmmetMode(document.languageId, []) || ''; @@ -55,8 +59,7 @@ function getRangesToReplace(document: vscode.TextDocument, nodeToUpdate: HtmlNod (emmetConfig.syntaxProfiles[emmetMode]['selfClosingStyle'] === 'xhtml' || emmetConfig.syntaxProfiles[emmetMode]['self_closing_tag'] === 'xhtml')) { textToReplaceWith = ' ' + textToReplaceWith; } - } return new vscode.TextEdit(rangeToReplace, textToReplaceWith); -} \ No newline at end of file +} diff --git a/extensions/emmet/src/test/editPointSelectItemBalance.test.ts b/extensions/emmet/src/test/editPointSelectItemBalance.test.ts index e5960d5f..e429fe7e 100644 --- a/extensions/emmet/src/test/editPointSelectItemBalance.test.ts +++ b/extensions/emmet/src/test/editPointSelectItemBalance.test.ts @@ -62,13 +62,13 @@ suite('Tests for Next/Previous Select/Edit point and Balance actions', () => { return withRandomFileEditor(htmlContents, '.html', (editor, _) => { editor.selections = [new Selection(1, 5, 1, 5)]; - let expectedNextEditPoints: [number, number][] = [[4, 16], [6, 8], [10, 2], [20, 0]]; + let expectedNextEditPoints: [number, number][] = [[4, 16], [6, 8], [10, 2], [10, 2]]; expectedNextEditPoints.forEach(([line, col]) => { fetchEditPoint('next'); testSelection(editor.selection, col, line); }); - let expectedPrevEditPoints = [[10, 2], [6, 8], [4, 16], [0, 0]]; + let expectedPrevEditPoints = [[6, 8], [4, 16], [4, 16]]; expectedPrevEditPoints.forEach(([line, col]) => { fetchEditPoint('prev'); testSelection(editor.selection, col, line); @@ -113,7 +113,7 @@ suite('Tests for Next/Previous Select/Edit point and Balance actions', () => { }); }); - test('Emmet Select Next/Prev item at boundary', function(): any { + test('Emmet Select Next/Prev item at boundary', function (): any { return withRandomFileEditor(htmlContents, '.html', (editor, _) => { editor.selections = [new Selection(4, 1, 4, 1)]; @@ -365,4 +365,4 @@ function testSelection(selection: Selection, startChar: number, startline: numbe } else { assert.equal(selection.active.character, endChar); } -} \ No newline at end of file +} diff --git a/extensions/emmet/src/test/index.ts b/extensions/emmet/src/test/index.ts index 3aeda9df..7d566743 100644 --- a/extensions/emmet/src/test/index.ts +++ b/extensions/emmet/src/test/index.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ const path = require('path'); -const testRunner = require('vscode/lib/testrunner'); +const testRunner = require('../../../../test/integration/electron/testrunner'); const options: any = { ui: 'tdd', @@ -38,3 +38,34 @@ if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { testRunner.configure(options); export = testRunner; + +// import * as path from 'path'; +// import * as Mocha from 'mocha'; +// import * as glob from 'glob'; + +// export function run(testsRoot: string, cb: (error: any, failures?: number) => void): void { +// // Create the mocha test +// const mocha = new Mocha({ +// ui: 'tdd' +// }); +// mocha.useColors(true); + +// glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { +// if (err) { +// return cb(err); +// } + +// // Add files to the test suite +// files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); + +// try { +// // Run the mocha test +// mocha.run(failures => { +// cb(null, failures); +// }); +// } catch (err) { +// console.error(err); +// cb(err); +// } +// }); +// } diff --git a/extensions/emmet/src/test/partialParsingStylesheet.test.ts b/extensions/emmet/src/test/partialParsingStylesheet.test.ts index 0c19d06b..b0928e1a 100644 --- a/extensions/emmet/src/test/partialParsingStylesheet.test.ts +++ b/extensions/emmet/src/test/partialParsingStylesheet.test.ts @@ -7,15 +7,16 @@ import 'mocha'; import * as assert from 'assert'; import { withRandomFileEditor } from './testUtils'; import * as vscode from 'vscode'; -import { parsePartialStylesheet, getNode } from '../util'; +import { parsePartialStylesheet, getFlatNode } from '../util'; import { isValidLocationForEmmetAbbreviation } from '../abbreviationActions'; suite('Tests for partial parse of Stylesheets', () => { function isValid(doc: vscode.TextDocument, range: vscode.Range, syntax: string): boolean { const rootNode = parsePartialStylesheet(doc, range.end); - const currentNode = getNode(rootNode, range.end, true); - return isValidLocationForEmmetAbbreviation(doc, rootNode, currentNode, syntax, range.end, range); + const endOffset = doc.offsetAt(range.end); + const currentNode = getFlatNode(rootNode, endOffset, true); + return isValidLocationForEmmetAbbreviation(doc, rootNode, currentNode, syntax, endOffset, range); } test('Ignore block comment inside rule', function (): any { @@ -257,4 +258,4 @@ ment */{ }); -}); \ No newline at end of file +}); diff --git a/extensions/emmet/src/test/tagActions.test.ts b/extensions/emmet/src/test/tagActions.test.ts index 80f2d30f..8c217bf3 100644 --- a/extensions/emmet/src/test/tagActions.test.ts +++ b/extensions/emmet/src/test/tagActions.test.ts @@ -175,7 +175,7 @@ suite('Tests for Emmet actions on html tags', () => {
  • Hello
  • There
  • Bye
  • -\t +\t\t `; diff --git a/extensions/emmet/src/test/toggleComment.test.ts b/extensions/emmet/src/test/toggleComment.test.ts index 4793c3ed..42a867f9 100644 --- a/extensions/emmet/src/test/toggleComment.test.ts +++ b/extensions/emmet/src/test/toggleComment.test.ts @@ -26,7 +26,7 @@ suite('Tests for Toggle Comment action from Emmet (HTML)', () => {
  • Bye
    • - +
    • Another Node
    @@ -47,24 +47,24 @@ suite('Tests for Toggle Comment action from Emmet (HTML)', () => { const expectedContents = `
      -
    • - - +
    • + +
    - + -->
    `; @@ -89,24 +89,24 @@ suite('Tests for Toggle Comment action from Emmet (HTML)', () => { const expectedContents = `
      -
    • - +
    • +
    • Bye
    - + -->
    `; @@ -130,19 +130,19 @@ suite('Tests for Toggle Comment action from Emmet (HTML)', () => { const expectedContents = `
      - +
    • Bye
      - +
    • Another Node
    --> + -->
    `; return withRandomFileEditor(contents, 'html', (editor, doc) => { @@ -252,16 +252,16 @@ suite('Tests for Toggle Comment action from Emmet (HTML)', () => { const templateContents = ` `; const expectedContents = ` `; @@ -298,13 +298,13 @@ suite('Tests for Toggle Comment action from Emmet (CSS)', () => { test('toggle comment with multiple cursors, but no selection (CSS)', () => { const expectedContents = ` .one { - /*margin: 10px;*/ + /* margin: 10px; */ padding: 10px; } - /*.two { + /* .two { height: 42px; display: none; - }*/ + } */ .three { width: 42px; }`; @@ -327,13 +327,13 @@ suite('Tests for Toggle Comment action from Emmet (CSS)', () => { test('toggle comment with multiple cursors and whole node selected (CSS)', () => { const expectedContents = ` .one { - /*margin: 10px;*/ - /*padding: 10px;*/ + /* margin: 10px; */ + /* padding: 10px; */ } - /*.two { + /* .two { height: 42px; display: none; - }*/ + } */ .three { width: 42px; }`; @@ -359,16 +359,16 @@ suite('Tests for Toggle Comment action from Emmet (CSS)', () => { test('toggle comment when multiple nodes of same parent are completely under single selection (CSS)', () => { const expectedContents = ` .one { -/* margin: 10px; - padding: 10px;*/ +/* margin: 10px; + padding: 10px; */ } - /*.two { + /* .two { height: 42px; display: none; } .three { width: 42px; - }*/`; + } */`; return withRandomFileEditor(contents, 'css', (editor, doc) => { editor.selections = [ new Selection(2, 0, 3, 16), // 2 properties completely under a single selection along with whitespace @@ -389,10 +389,10 @@ suite('Tests for Toggle Comment action from Emmet (CSS)', () => { const expectedContents = ` .one { margin: 10px; - /*padding: 10px; + /* padding: 10px; } .two { - height: 42px;*/ + height: 42px; */ display: none; } .three { @@ -417,10 +417,10 @@ suite('Tests for Toggle Comment action from Emmet (CSS)', () => { const expectedContents = ` .one { margin: 10px; - /*padding: 10px; + /* padding: 10px; } .two { - height: 42px;*/ + height: 42px; */ display: none; } .three { @@ -445,10 +445,10 @@ suite('Tests for Toggle Comment action from Emmet (CSS)', () => { const expectedContents = ` .one { margin: 10px; - /*padding: 10px; + /* padding: 10px; } .two { - height: 42px;*/ + height: 42px; */ display: none; } .three { @@ -473,10 +473,10 @@ suite('Tests for Toggle Comment action from Emmet (CSS)', () => { const expectedContents = ` .one { margin: 10px; - /*padding: 10px; + /* padding: 10px; } .two { - height: 42px;*/ + height: 42px; */ display: none; } .three { @@ -500,16 +500,16 @@ suite('Tests for Toggle Comment action from Emmet (CSS)', () => { test('toggle comment when multiple nodes of same parent are partially under single selection (CSS)', () => { const expectedContents = ` .one { - /*margin: 10px; - padding: 10px;*/ + /* margin: 10px; + padding: 10px; */ } - /*.two { + /* .two { height: 42px; display: none; } .three { width: 42px; -*/ }`; + */ }`; return withRandomFileEditor(contents, 'css', (editor, doc) => { editor.selections = [ new Selection(2, 7, 3, 10), // 2 properties partially under a single selection @@ -549,14 +549,14 @@ suite('Tests for Toggle Comment action from Emmet in nested css (SCSS)', () => { test('toggle comment with multiple cursors selecting nested nodes (SCSS)', () => { const expectedContents = ` .one { - /*height: 42px;*/ + /* height: 42px; */ - /*.two { + /* .two { width: 42px; - }*/ + } */ .three { - /*padding: 10px;*/ + /* padding: 10px; */ } }`; return withRandomFileEditor(contents, 'css', (editor, doc) => { @@ -578,7 +578,7 @@ suite('Tests for Toggle Comment action from Emmet in nested css (SCSS)', () => { }); test('toggle comment with multiple cursors selecting several nested nodes (SCSS)', () => { const expectedContents = ` - /*.one { + /* .one { height: 42px; .two { @@ -588,7 +588,7 @@ suite('Tests for Toggle Comment action from Emmet in nested css (SCSS)', () => { .three { padding: 10px; } - }*/`; + } */`; return withRandomFileEditor(contents, 'css', (editor, doc) => { editor.selections = [ new Selection(1, 3, 1, 3), // cursor in the outside rule. And several cursors inside: @@ -611,14 +611,14 @@ suite('Tests for Toggle Comment action from Emmet in nested css (SCSS)', () => { test('toggle comment with multiple cursors, but no selection (SCSS)', () => { const expectedContents = ` .one { - /*height: 42px;*/ + /* height: 42px; */ - /*.two { + /* .two { width: 42px; - }*/ + } */ .three { - /*padding: 10px;*/ + /* padding: 10px; */ } }`; return withRandomFileEditor(contents, 'css', (editor, doc) => { @@ -641,14 +641,14 @@ suite('Tests for Toggle Comment action from Emmet in nested css (SCSS)', () => { test('toggle comment with multiple cursors and whole node selected (CSS)', () => { const expectedContents = ` .one { - /*height: 42px;*/ + /* height: 42px; */ - /*.two { + /* .two { width: 42px; - }*/ + } */ .three { - /*padding: 10px;*/ + /* padding: 10px; */ } }`; return withRandomFileEditor(contents, 'css', (editor, doc) => { @@ -673,11 +673,11 @@ suite('Tests for Toggle Comment action from Emmet in nested css (SCSS)', () => { test('toggle comment when multiple nodes are completely under single selection (CSS)', () => { const expectedContents = ` .one { - /*height: 42px; + /* height: 42px; .two { width: 42px; - }*/ + } */ .three { padding: 10px; @@ -701,11 +701,11 @@ suite('Tests for Toggle Comment action from Emmet in nested css (SCSS)', () => { test('toggle comment when multiple nodes are partially under single selection (CSS)', () => { const expectedContents = ` .one { - /*height: 42px; + /* height: 42px; .two { width: 42px; - */ } + */ } .three { padding: 10px; @@ -726,4 +726,29 @@ suite('Tests for Toggle Comment action from Emmet in nested css (SCSS)', () => { }); }); -}); \ No newline at end of file + test('toggle comment doesn\'t fail when start and end nodes differ HTML', () => { + const contents = ` +
    +

    Hello

    +
    + `; + const expectedContents = ` + + `; + return withRandomFileEditor(contents, 'html', (editor, doc) => { + editor.selections = [ + new Selection(1, 2, 2, 9), //
    to

    inclusive + ]; + + return toggleComment().then(() => { + assert.equal(doc.getText(), expectedContents); + return toggleComment().then(() => { + assert.equal(doc.getText(), contents); + return Promise.resolve(); + }); + }); + }); + }); +}); diff --git a/extensions/emmet/src/test/updateImageSize.test.ts b/extensions/emmet/src/test/updateImageSize.test.ts index 63452786..b43b3e6e 100644 --- a/extensions/emmet/src/test/updateImageSize.test.ts +++ b/extensions/emmet/src/test/updateImageSize.test.ts @@ -3,148 +3,147 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// import 'mocha'; -// import * as assert from 'assert'; -// import { Selection } from 'vscode'; -// import { withRandomFileEditor, closeAllEditors } from './testUtils'; -// import { updateImageSize } from '../updateImageSize'; +import 'mocha'; +import * as assert from 'assert'; +import { Selection } from 'vscode'; +import { withRandomFileEditor, closeAllEditors } from './testUtils'; +import { updateImageSize } from '../updateImageSize'; -// suite('Tests for Emmet actions on html tags', () => { -// teardown(closeAllEditors); +suite('Tests for Emmet actions on html tags', () => { + teardown(closeAllEditors); - // test('update image css with multiple cursors in css file', () => { - // const cssContents = ` - // .one { - // margin: 10px; - // padding: 10px; - // background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png); - // } - // .two { - // background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png); - // height: 42px; - // } - // .three { - // background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png); - // width: 42px; - // } - // `; - // const expectedContents = ` - // .one { - // margin: 10px; - // padding: 10px; - // background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png); - // width: 32px; - // height: 32px; - // } - // .two { - // background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png); - // width: 32px; - // height: 32px; - // } - // .three { - // background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png); - // height: 32px; - // width: 32px; - // } - // `; - // return withRandomFileEditor(cssContents, 'css', (editor, doc) => { - // editor.selections = [ - // new Selection(4, 50, 4, 50), - // new Selection(7, 50, 7, 50), - // new Selection(11, 50, 11, 50) - // ]; + test('update image css with multiple cursors in css file', () => { + const cssContents = ` + .one { + margin: 10px; + padding: 10px; + background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); + } + .two { + background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); + height: 42px; + } + .three { + background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); + width: 42px; + } + `; + const expectedContents = ` + .one { + margin: 10px; + padding: 10px; + background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); + width: 1024px; + height: 1024px; + } + .two { + background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); + width: 1024px; + height: 1024px; + } + .three { + background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); + height: 1024px; + width: 1024px; + } + `; + return withRandomFileEditor(cssContents, 'css', (editor, doc) => { + editor.selections = [ + new Selection(4, 50, 4, 50), + new Selection(7, 50, 7, 50), + new Selection(11, 50, 11, 50) + ]; - // return updateImageSize()!.then(() => { - // assert.equal(doc.getText(), expectedContents); - // return Promise.resolve(); - // }); - // }); - // }); + return updateImageSize()!.then(() => { + assert.equal(doc.getText(), expectedContents); + return Promise.resolve(); + }); + }); + }); - // test('update image size in css in html file with multiple cursors', () => { - // const htmlWithCssContents = ` - // - // - // - // `; - // const expectedContents = ` - // - // - // - // `; - // return withRandomFileEditor(htmlWithCssContents, 'html', (editor, doc) => { - // editor.selections = [ - // new Selection(6, 50, 6, 50), - // new Selection(9, 50, 9, 50), - // new Selection(13, 50, 13, 50) - // ]; + test('update image size in css in html file with multiple cursors', () => { + const htmlWithCssContents = ` + + + + `; + const expectedContents = ` + + + + `; + return withRandomFileEditor(htmlWithCssContents, 'html', (editor, doc) => { + editor.selections = [ + new Selection(6, 50, 6, 50), + new Selection(9, 50, 9, 50), + new Selection(13, 50, 13, 50) + ]; - // return updateImageSize()!.then(() => { - // assert.equal(doc.getText(), expectedContents); - // return Promise.resolve(); - // }); - // }); - // }); + return updateImageSize()!.then(() => { + assert.equal(doc.getText(), expectedContents); + return Promise.resolve(); + }); + }); + }); - // test('update image size in img tag in html file with multiple cursors', () => { - // const htmlwithimgtag = ` - // - // - // - // - // - // `; - // const expectedContents = ` - // - // - // - // - // - // `; - // return withRandomFileEditor(htmlwithimgtag, 'html', (editor, doc) => { - // editor.selections = [ - // new Selection(2, 50, 2, 50), - // new Selection(3, 50, 3, 50), - // new Selection(4, 50, 4, 50) - // ]; + test('update image size in img tag in html file with multiple cursors', () => { + const htmlwithimgtag = ` + + + + + + `; + const expectedContents = ` + + + + + + `; + return withRandomFileEditor(htmlwithimgtag, 'html', (editor, doc) => { + editor.selections = [ + new Selection(2, 50, 2, 50), + new Selection(3, 50, 3, 50), + new Selection(4, 50, 4, 50) + ]; - // return updateImageSize()!.then(() => { - // assert.equal(doc.getText(), expectedContents); - // return Promise.resolve(); - // }); - // }); - // }); - -// }); + return updateImageSize()!.then(() => { + assert.equal(doc.getText(), expectedContents); + return Promise.resolve(); + }); + }); + }); +}); diff --git a/extensions/emmet/src/test/wrapWithAbbreviation.test.ts b/extensions/emmet/src/test/wrapWithAbbreviation.test.ts index d5a4a2bc..c89e6942 100644 --- a/extensions/emmet/src/test/wrapWithAbbreviation.test.ts +++ b/extensions/emmet/src/test/wrapWithAbbreviation.test.ts @@ -9,13 +9,20 @@ import { Selection, workspace, ConfigurationTarget } from 'vscode'; import { withRandomFileEditor, closeAllEditors } from './testUtils'; import { wrapWithAbbreviation, wrapIndividualLinesWithAbbreviation } from '../abbreviationActions'; -const htmlContentsForWrapTests = ` +const htmlContentsForBlockWrapTests = `

    `; +const htmlContentsForInlineWrapTests = ` + +`; + const wrapBlockElementExpected = ` `; +// technically a bug, but also a feature (requested behaviour) +// https://github.com/microsoft/vscode/issues/78015 const wrapInlineElementExpectedFormatFalse = ` `; @@ -73,51 +90,51 @@ suite('Tests for Wrap with Abbreviations', () => { const oldValueForSyntaxProfiles = workspace.getConfiguration('emmet').inspect('syntaxProfile'); test('Wrap with block element using multi cursor', () => { - return testWrapWithAbbreviation(multiCursors, 'div', wrapBlockElementExpected); + return testWrapWithAbbreviation(multiCursors, 'div', wrapBlockElementExpected, htmlContentsForBlockWrapTests); }); test('Wrap with inline element using multi cursor', () => { - return testWrapWithAbbreviation(multiCursors, 'span', wrapInlineElementExpected); + return testWrapWithAbbreviation(multiCursors, 'span', wrapInlineElementExpected, htmlContentsForInlineWrapTests); }); test('Wrap with snippet using multi cursor', () => { - return testWrapWithAbbreviation(multiCursors, 'a', wrapSnippetExpected); + return testWrapWithAbbreviation(multiCursors, 'a', wrapSnippetExpected, htmlContentsForBlockWrapTests); }); test('Wrap with multi line abbreviation using multi cursor', () => { - return testWrapWithAbbreviation(multiCursors, 'ul>li', wrapMultiLineAbbrExpected); + return testWrapWithAbbreviation(multiCursors, 'ul>li', wrapMultiLineAbbrExpected, htmlContentsForBlockWrapTests); }); test('Wrap with block element using multi cursor selection', () => { - return testWrapWithAbbreviation(multiCursorsWithSelection, 'div', wrapBlockElementExpected); + return testWrapWithAbbreviation(multiCursorsWithSelection, 'div', wrapBlockElementExpected, htmlContentsForBlockWrapTests); }); test('Wrap with inline element using multi cursor selection', () => { - return testWrapWithAbbreviation(multiCursorsWithSelection, 'span', wrapInlineElementExpected); + return testWrapWithAbbreviation(multiCursorsWithSelection, 'span', wrapInlineElementExpected, htmlContentsForInlineWrapTests); }); test('Wrap with snippet using multi cursor selection', () => { - return testWrapWithAbbreviation(multiCursorsWithSelection, 'a', wrapSnippetExpected); + return testWrapWithAbbreviation(multiCursorsWithSelection, 'a', wrapSnippetExpected, htmlContentsForBlockWrapTests); }); test('Wrap with multi line abbreviation using multi cursor selection', () => { - return testWrapWithAbbreviation(multiCursorsWithSelection, 'ul>li', wrapMultiLineAbbrExpected); + return testWrapWithAbbreviation(multiCursorsWithSelection, 'ul>li', wrapMultiLineAbbrExpected, htmlContentsForBlockWrapTests); }); test('Wrap with block element using multi cursor full line selection', () => { - return testWrapWithAbbreviation(multiCursorsWithFullLineSelection, 'div', wrapBlockElementExpected); + return testWrapWithAbbreviation(multiCursorsWithFullLineSelection, 'div', wrapBlockElementExpected, htmlContentsForBlockWrapTests); }); test('Wrap with inline element using multi cursor full line selection', () => { - return testWrapWithAbbreviation(multiCursorsWithFullLineSelection, 'span', wrapInlineElementExpected); + return testWrapWithAbbreviation(multiCursorsWithFullLineSelection, 'span', wrapInlineElementExpected, htmlContentsForInlineWrapTests); }); test('Wrap with snippet using multi cursor full line selection', () => { - return testWrapWithAbbreviation(multiCursorsWithFullLineSelection, 'a', wrapSnippetExpected); + return testWrapWithAbbreviation(multiCursorsWithFullLineSelection, 'a', wrapSnippetExpected, htmlContentsForBlockWrapTests); }); test('Wrap with multi line abbreviation using multi cursor full line selection', () => { - return testWrapWithAbbreviation(multiCursorsWithFullLineSelection, 'ul>li', wrapMultiLineAbbrExpected); + return testWrapWithAbbreviation(multiCursorsWithFullLineSelection, 'ul>li', wrapMultiLineAbbrExpected, htmlContentsForBlockWrapTests); }); test('Wrap with abbreviation and comment filter', () => { @@ -128,15 +145,31 @@ suite('Tests for Wrap with Abbreviations', () => { `; const expectedContents = ` `; return testWrapWithAbbreviation([new Selection(2, 0, 2, 0)], 'li.hello|c', expectedContents, contents); }); + test('Wrap with abbreviation link', () => { + const contents = ` + + `; + const expectedContents = ` + +
    + +
    +
    + `; + return testWrapWithAbbreviation([new Selection(1, 1, 1, 1)], 'a[href="https://example.com"]>div', expectedContents, contents); + }); + test('Wrap with abbreviation entire node when cursor is on opening tag', () => { const contents = ` ', 'adiv classname="">', fuzzyScore); + }); + test('Suggestion is not highlighted #85826', function () { assertMatches('SemanticTokens', 'SemanticTokensEdits', '^S^e^m^a^n^t^i^c^T^o^k^e^n^sEdits', fuzzyScore); assertMatches('SemanticTokens', 'SemanticTokensEdits', '^S^e^m^a^n^t^i^c^T^o^k^e^n^sEdits', fuzzyScoreGracefulAggressive); }); + + test('IntelliSense completion not correctly highlighting text in front of cursor #115250', function () { + assertMatches('lo', 'log', '^l^og', fuzzyScore); + assertMatches('.lo', 'log', '^l^og', anyScore); + assertMatches('.', 'log', 'log', anyScore); + }); }); diff --git a/src/vs/base/test/common/fuzzyScorer.test.ts b/src/vs/base/test/common/fuzzyScorer.test.ts index 5aac1fb9..59c52ce0 100644 --- a/src/vs/base/test/common/fuzzyScorer.test.ts +++ b/src/vs/base/test/common/fuzzyScorer.test.ts @@ -120,30 +120,30 @@ suite('Fuzzy Scorer', () => { // Assert scoring order let sortedScores = scores.concat().sort((a, b) => b[0] - a[0]); - assert.deepEqual(scores, sortedScores); + assert.deepStrictEqual(scores, sortedScores); // Assert scoring positions // let positions = scores[0][1]; - // assert.equal(positions.length, 'HelLo-World'.length); + // assert.strictEqual(positions.length, 'HelLo-World'.length); // positions = scores[2][1]; - // assert.equal(positions.length, 'HW'.length); - // assert.equal(positions[0], 0); - // assert.equal(positions[1], 6); + // assert.strictEqual(positions.length, 'HW'.length); + // assert.strictEqual(positions[0], 0); + // assert.strictEqual(positions[1], 6); }); test('score (non fuzzy)', function () { const target = 'HeLlo-World'; assert.ok(_doScore(target, 'HelLo-World', false)[0] > 0); - assert.equal(_doScore(target, 'HelLo-World', false)[1].length, 'HelLo-World'.length); + assert.strictEqual(_doScore(target, 'HelLo-World', false)[1].length, 'HelLo-World'.length); assert.ok(_doScore(target, 'hello-world', false)[0] > 0); - assert.equal(_doScore(target, 'HW', false)[0], 0); + assert.strictEqual(_doScore(target, 'HW', false)[0], 0); assert.ok(_doScore(target, 'h', false)[0] > 0); assert.ok(_doScore(target, 'ello', false)[0] > 0); assert.ok(_doScore(target, 'ld', false)[0] > 0); - assert.equal(_doScore(target, 'eo', false)[0], 0); + assert.strictEqual(_doScore(target, 'eo', false)[0], 0); }); test('scoreItem - matches are proper', function () { @@ -158,52 +158,52 @@ suite('Fuzzy Scorer', () => { // Path Identity const identityRes = scoreItem(resource, ResourceAccessor.getItemPath(resource), true, ResourceAccessor); assert.ok(identityRes.score); - assert.equal(identityRes.descriptionMatch!.length, 1); - assert.equal(identityRes.labelMatch!.length, 1); - assert.equal(identityRes.descriptionMatch![0].start, 0); - assert.equal(identityRes.descriptionMatch![0].end, ResourceAccessor.getItemDescription(resource).length); - assert.equal(identityRes.labelMatch![0].start, 0); - assert.equal(identityRes.labelMatch![0].end, ResourceAccessor.getItemLabel(resource).length); + assert.strictEqual(identityRes.descriptionMatch!.length, 1); + assert.strictEqual(identityRes.labelMatch!.length, 1); + assert.strictEqual(identityRes.descriptionMatch![0].start, 0); + assert.strictEqual(identityRes.descriptionMatch![0].end, ResourceAccessor.getItemDescription(resource).length); + assert.strictEqual(identityRes.labelMatch![0].start, 0); + assert.strictEqual(identityRes.labelMatch![0].end, ResourceAccessor.getItemLabel(resource).length); // Basename Prefix const basenamePrefixRes = scoreItem(resource, 'som', true, ResourceAccessor); assert.ok(basenamePrefixRes.score); assert.ok(!basenamePrefixRes.descriptionMatch); - assert.equal(basenamePrefixRes.labelMatch!.length, 1); - assert.equal(basenamePrefixRes.labelMatch![0].start, 0); - assert.equal(basenamePrefixRes.labelMatch![0].end, 'som'.length); + assert.strictEqual(basenamePrefixRes.labelMatch!.length, 1); + assert.strictEqual(basenamePrefixRes.labelMatch![0].start, 0); + assert.strictEqual(basenamePrefixRes.labelMatch![0].end, 'som'.length); // Basename Camelcase const basenameCamelcaseRes = scoreItem(resource, 'sF', true, ResourceAccessor); assert.ok(basenameCamelcaseRes.score); assert.ok(!basenameCamelcaseRes.descriptionMatch); - assert.equal(basenameCamelcaseRes.labelMatch!.length, 2); - assert.equal(basenameCamelcaseRes.labelMatch![0].start, 0); - assert.equal(basenameCamelcaseRes.labelMatch![0].end, 1); - assert.equal(basenameCamelcaseRes.labelMatch![1].start, 4); - assert.equal(basenameCamelcaseRes.labelMatch![1].end, 5); + assert.strictEqual(basenameCamelcaseRes.labelMatch!.length, 2); + assert.strictEqual(basenameCamelcaseRes.labelMatch![0].start, 0); + assert.strictEqual(basenameCamelcaseRes.labelMatch![0].end, 1); + assert.strictEqual(basenameCamelcaseRes.labelMatch![1].start, 4); + assert.strictEqual(basenameCamelcaseRes.labelMatch![1].end, 5); // Basename Match const basenameRes = scoreItem(resource, 'of', true, ResourceAccessor); assert.ok(basenameRes.score); assert.ok(!basenameRes.descriptionMatch); - assert.equal(basenameRes.labelMatch!.length, 2); - assert.equal(basenameRes.labelMatch![0].start, 1); - assert.equal(basenameRes.labelMatch![0].end, 2); - assert.equal(basenameRes.labelMatch![1].start, 4); - assert.equal(basenameRes.labelMatch![1].end, 5); + assert.strictEqual(basenameRes.labelMatch!.length, 2); + assert.strictEqual(basenameRes.labelMatch![0].start, 1); + assert.strictEqual(basenameRes.labelMatch![0].end, 2); + assert.strictEqual(basenameRes.labelMatch![1].start, 4); + assert.strictEqual(basenameRes.labelMatch![1].end, 5); // Path Match const pathRes = scoreItem(resource, 'xyz123', true, ResourceAccessor); assert.ok(pathRes.score); assert.ok(pathRes.descriptionMatch); assert.ok(pathRes.labelMatch); - assert.equal(pathRes.labelMatch!.length, 1); - assert.equal(pathRes.labelMatch![0].start, 8); - assert.equal(pathRes.labelMatch![0].end, 11); - assert.equal(pathRes.descriptionMatch!.length, 1); - assert.equal(pathRes.descriptionMatch![0].start, 1); - assert.equal(pathRes.descriptionMatch![0].end, 4); + assert.strictEqual(pathRes.labelMatch!.length, 1); + assert.strictEqual(pathRes.labelMatch![0].start, 8); + assert.strictEqual(pathRes.labelMatch![0].end, 11); + assert.strictEqual(pathRes.descriptionMatch!.length, 1); + assert.strictEqual(pathRes.descriptionMatch![0].start, 1); + assert.strictEqual(pathRes.descriptionMatch![0].end, 4); // No Match const noRes = scoreItem(resource, '987', true, ResourceAccessor); @@ -223,51 +223,51 @@ suite('Fuzzy Scorer', () => { let res1 = scoreItem(resource, 'xyz some', true, ResourceAccessor); assert.ok(res1.score); - assert.equal(res1.labelMatch?.length, 1); - assert.equal(res1.labelMatch![0].start, 0); - assert.equal(res1.labelMatch![0].end, 4); - assert.equal(res1.descriptionMatch?.length, 1); - assert.equal(res1.descriptionMatch![0].start, 1); - assert.equal(res1.descriptionMatch![0].end, 4); + assert.strictEqual(res1.labelMatch?.length, 1); + assert.strictEqual(res1.labelMatch![0].start, 0); + assert.strictEqual(res1.labelMatch![0].end, 4); + assert.strictEqual(res1.descriptionMatch?.length, 1); + assert.strictEqual(res1.descriptionMatch![0].start, 1); + assert.strictEqual(res1.descriptionMatch![0].end, 4); let res2 = scoreItem(resource, 'some xyz', true, ResourceAccessor); assert.ok(res2.score); - assert.equal(res1.score, res2.score); - assert.equal(res2.labelMatch?.length, 1); - assert.equal(res2.labelMatch![0].start, 0); - assert.equal(res2.labelMatch![0].end, 4); - assert.equal(res2.descriptionMatch?.length, 1); - assert.equal(res2.descriptionMatch![0].start, 1); - assert.equal(res2.descriptionMatch![0].end, 4); + assert.strictEqual(res1.score, res2.score); + assert.strictEqual(res2.labelMatch?.length, 1); + assert.strictEqual(res2.labelMatch![0].start, 0); + assert.strictEqual(res2.labelMatch![0].end, 4); + assert.strictEqual(res2.descriptionMatch?.length, 1); + assert.strictEqual(res2.descriptionMatch![0].start, 1); + assert.strictEqual(res2.descriptionMatch![0].end, 4); let res3 = scoreItem(resource, 'some xyz file file123', true, ResourceAccessor); assert.ok(res3.score); assert.ok(res3.score > res2.score); - assert.equal(res3.labelMatch?.length, 1); - assert.equal(res3.labelMatch![0].start, 0); - assert.equal(res3.labelMatch![0].end, 11); - assert.equal(res3.descriptionMatch?.length, 1); - assert.equal(res3.descriptionMatch![0].start, 1); - assert.equal(res3.descriptionMatch![0].end, 4); + assert.strictEqual(res3.labelMatch?.length, 1); + assert.strictEqual(res3.labelMatch![0].start, 0); + assert.strictEqual(res3.labelMatch![0].end, 11); + assert.strictEqual(res3.descriptionMatch?.length, 1); + assert.strictEqual(res3.descriptionMatch![0].start, 1); + assert.strictEqual(res3.descriptionMatch![0].end, 4); let res4 = scoreItem(resource, 'path z y', true, ResourceAccessor); assert.ok(res4.score); assert.ok(res4.score < res2.score); - assert.equal(res4.labelMatch?.length, 0); - assert.equal(res4.descriptionMatch?.length, 2); - assert.equal(res4.descriptionMatch![0].start, 2); - assert.equal(res4.descriptionMatch![0].end, 4); - assert.equal(res4.descriptionMatch![1].start, 10); - assert.equal(res4.descriptionMatch![1].end, 14); + assert.strictEqual(res4.labelMatch?.length, 0); + assert.strictEqual(res4.descriptionMatch?.length, 2); + assert.strictEqual(res4.descriptionMatch![0].start, 2); + assert.strictEqual(res4.descriptionMatch![0].end, 4); + assert.strictEqual(res4.descriptionMatch![1].start, 10); + assert.strictEqual(res4.descriptionMatch![1].end, 14); }); test('scoreItem - invalid input', function () { let res = scoreItem(null, null!, true, ResourceAccessor); - assert.equal(res.score, 0); + assert.strictEqual(res.score, 0); res = scoreItem(null, 'null', true, ResourceAccessor); - assert.equal(res.score, 0); + assert.strictEqual(res.score, 0); }); test('scoreItem - optimize for file paths', function () { @@ -280,12 +280,12 @@ suite('Fuzzy Scorer', () => { assert.ok(pathRes.score); assert.ok(pathRes.descriptionMatch); assert.ok(pathRes.labelMatch); - assert.equal(pathRes.labelMatch!.length, 1); - assert.equal(pathRes.labelMatch![0].start, 0); - assert.equal(pathRes.labelMatch![0].end, 7); - assert.equal(pathRes.descriptionMatch!.length, 1); - assert.equal(pathRes.descriptionMatch![0].start, 23); - assert.equal(pathRes.descriptionMatch![0].end, 26); + assert.strictEqual(pathRes.labelMatch!.length, 1); + assert.strictEqual(pathRes.labelMatch![0].start, 0); + assert.strictEqual(pathRes.labelMatch![0].end, 7); + assert.strictEqual(pathRes.descriptionMatch!.length, 1); + assert.strictEqual(pathRes.descriptionMatch![0].start, 23); + assert.strictEqual(pathRes.descriptionMatch![0].end, 26); }); test('scoreItem - avoid match scattering (bug #36119)', function () { @@ -295,9 +295,9 @@ suite('Fuzzy Scorer', () => { assert.ok(pathRes.score); assert.ok(pathRes.descriptionMatch); assert.ok(pathRes.labelMatch); - assert.equal(pathRes.labelMatch!.length, 1); - assert.equal(pathRes.labelMatch![0].start, 0); - assert.equal(pathRes.labelMatch![0].end, 9); + assert.strictEqual(pathRes.labelMatch!.length, 1); + assert.strictEqual(pathRes.labelMatch![0].start, 0); + assert.strictEqual(pathRes.labelMatch![0].end, 9); }); test('scoreItem - prefers more compact matches', function () { @@ -309,11 +309,11 @@ suite('Fuzzy Scorer', () => { assert.ok(res.score); assert.ok(res.descriptionMatch); assert.ok(!res.labelMatch!.length); - assert.equal(res.descriptionMatch!.length, 2); - assert.equal(res.descriptionMatch![0].start, 11); - assert.equal(res.descriptionMatch![0].end, 12); - assert.equal(res.descriptionMatch![1].start, 13); - assert.equal(res.descriptionMatch![1].end, 14); + assert.strictEqual(res.descriptionMatch!.length, 2); + assert.strictEqual(res.descriptionMatch![0].start, 11); + assert.strictEqual(res.descriptionMatch![0].end, 12); + assert.strictEqual(res.descriptionMatch![1].start, 13); + assert.strictEqual(res.descriptionMatch![1].end, 14); }); test('scoreItem - proper target offset', function () { @@ -328,9 +328,9 @@ suite('Fuzzy Scorer', () => { const res = scoreItem(resource, 'de', true, ResourceAccessor); - assert.equal(res.labelMatch!.length, 1); - assert.equal(res.labelMatch![0].start, 1); - assert.equal(res.labelMatch![0].end, 3); + assert.strictEqual(res.labelMatch!.length, 1); + assert.strictEqual(res.labelMatch![0].start, 1); + assert.strictEqual(res.labelMatch![0].end, 3); }); test('scoreItem - proper target offset #3', function () { @@ -338,19 +338,19 @@ suite('Fuzzy Scorer', () => { const res = scoreItem(resource, 'debug', true, ResourceAccessor); - assert.equal(res.descriptionMatch!.length, 3); - assert.equal(res.descriptionMatch![0].start, 9); - assert.equal(res.descriptionMatch![0].end, 10); - assert.equal(res.descriptionMatch![1].start, 36); - assert.equal(res.descriptionMatch![1].end, 37); - assert.equal(res.descriptionMatch![2].start, 40); - assert.equal(res.descriptionMatch![2].end, 41); + assert.strictEqual(res.descriptionMatch!.length, 3); + assert.strictEqual(res.descriptionMatch![0].start, 9); + assert.strictEqual(res.descriptionMatch![0].end, 10); + assert.strictEqual(res.descriptionMatch![1].start, 36); + assert.strictEqual(res.descriptionMatch![1].end, 37); + assert.strictEqual(res.descriptionMatch![2].start, 40); + assert.strictEqual(res.descriptionMatch![2].end, 41); - assert.equal(res.labelMatch!.length, 2); - assert.equal(res.labelMatch![0].start, 9); - assert.equal(res.labelMatch![0].end, 10); - assert.equal(res.labelMatch![1].start, 20); - assert.equal(res.labelMatch![1].end, 21); + assert.strictEqual(res.labelMatch!.length, 2); + assert.strictEqual(res.labelMatch![0].start, 9); + assert.strictEqual(res.labelMatch![0].end, 10); + assert.strictEqual(res.labelMatch![1].start, 20); + assert.strictEqual(res.labelMatch![1].end, 21); }); test('scoreItem - no match unless query contained in sequence', function () { @@ -394,27 +394,27 @@ suite('Fuzzy Scorer', () => { let query = ResourceAccessor.getItemPath(resourceA); let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); // Full resource B path query = ResourceAccessor.getItemPath(resourceB); res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - basename prefix', function () { @@ -426,27 +426,27 @@ suite('Fuzzy Scorer', () => { let query = ResourceAccessor.getItemLabel(resourceA); let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); // Full resource B basename query = ResourceAccessor.getItemLabel(resourceB); res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - basename camelcase', function () { @@ -458,27 +458,27 @@ suite('Fuzzy Scorer', () => { let query = 'fA'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); // resource B camelcase query = 'fB'; res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - basename scores', function () { @@ -490,27 +490,27 @@ suite('Fuzzy Scorer', () => { let query = 'fileA'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); // Resource B part of basename query = 'fileB'; res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - path scores', function () { @@ -522,27 +522,27 @@ suite('Fuzzy Scorer', () => { let query = 'pathfileA'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); // Resource B part of path query = 'pathfileB'; res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - prefer shorter basenames', function () { @@ -554,14 +554,14 @@ suite('Fuzzy Scorer', () => { let query = 'somepath'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - prefer shorter basenames (match on basename)', function () { @@ -573,14 +573,14 @@ suite('Fuzzy Scorer', () => { let query = 'file'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceC); - assert.equal(res[2], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceC); + assert.strictEqual(res[2], resourceB); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceC); - assert.equal(res[2], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceC); + assert.strictEqual(res[2], resourceB); }); test('compareFilesByScore - prefer shorter paths', function () { @@ -592,14 +592,14 @@ suite('Fuzzy Scorer', () => { let query = 'somepath'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - prefer shorter paths (bug #17443)', function () { @@ -610,9 +610,9 @@ suite('Fuzzy Scorer', () => { let query = 'co/te'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - prefer matches in label over description if scores are otherwise equal', function () { @@ -622,8 +622,8 @@ suite('Fuzzy Scorer', () => { let query = 'partsquick'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); }); test('compareFilesByScore - prefer camel case matches', function () { @@ -632,12 +632,12 @@ suite('Fuzzy Scorer', () => { for (const query of ['npe', 'NPE']) { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); } }); @@ -648,12 +648,12 @@ suite('Fuzzy Scorer', () => { let query = 'AH'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); }); test('compareFilesByScore - prefer more compact matches (label)', function () { @@ -663,12 +663,12 @@ suite('Fuzzy Scorer', () => { let query = 'xp'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); }); test('compareFilesByScore - prefer more compact matches (path)', function () { @@ -678,12 +678,12 @@ suite('Fuzzy Scorer', () => { let query = 'xp'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); }); test('compareFilesByScore - prefer more compact matches (label and path)', function () { @@ -693,12 +693,12 @@ suite('Fuzzy Scorer', () => { let query = 'exfile'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); }); test('compareFilesByScore - avoid match scattering (bug #34210)', function () { @@ -710,18 +710,18 @@ suite('Fuzzy Scorer', () => { let query = isWindows ? 'modu1\\index.js' : 'modu1/index.js'; let res = [resourceA, resourceB, resourceC, resourceD].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceC); + assert.strictEqual(res[0], resourceC); res = [resourceC, resourceB, resourceA, resourceD].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceC); + assert.strictEqual(res[0], resourceC); query = isWindows ? 'un1\\index.js' : 'un1/index.js'; res = [resourceA, resourceB, resourceC, resourceD].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceC, resourceB, resourceA, resourceD].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #21019 1.)', function () { @@ -732,10 +732,10 @@ suite('Fuzzy Scorer', () => { let query = 'StatVideoindex'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceC); + assert.strictEqual(res[0], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceC); + assert.strictEqual(res[0], resourceC); }); test('compareFilesByScore - avoid match scattering (bug #21019 2.)', function () { @@ -745,10 +745,10 @@ suite('Fuzzy Scorer', () => { let query = 'reproreduxts'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #26649)', function () { @@ -759,10 +759,10 @@ suite('Fuzzy Scorer', () => { let query = 'bookpageIndex'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceC); + assert.strictEqual(res[0], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceC); + assert.strictEqual(res[0], resourceC); }); test('compareFilesByScore - avoid match scattering (bug #33247)', function () { @@ -772,10 +772,10 @@ suite('Fuzzy Scorer', () => { let query = isWindows ? 'ui\\icons' : 'ui/icons'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #33247 comment)', function () { @@ -785,10 +785,10 @@ suite('Fuzzy Scorer', () => { let query = isWindows ? 'ui\\input\\index' : 'ui/input/index'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #36166)', function () { @@ -798,10 +798,10 @@ suite('Fuzzy Scorer', () => { let query = 'djancosig'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #32918)', function () { @@ -812,14 +812,14 @@ suite('Fuzzy Scorer', () => { let query = 'protectedconfig.php'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceC); - assert.equal(res[2], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceC); + assert.strictEqual(res[2], resourceB); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceC); - assert.equal(res[2], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceC); + assert.strictEqual(res[2], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #14879)', function () { @@ -829,10 +829,10 @@ suite('Fuzzy Scorer', () => { let query = 'gradientmain'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #14727 1)', function () { @@ -842,10 +842,10 @@ suite('Fuzzy Scorer', () => { let query = 'abc'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #14727 2)', function () { @@ -855,10 +855,10 @@ suite('Fuzzy Scorer', () => { let query = 'xyz'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #18381)', function () { @@ -868,10 +868,10 @@ suite('Fuzzy Scorer', () => { let query = 'async'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #35572)', function () { @@ -881,10 +881,10 @@ suite('Fuzzy Scorer', () => { let query = 'partisettings'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #36810)', function () { @@ -894,10 +894,10 @@ suite('Fuzzy Scorer', () => { let query = 'tipsindex.cshtml'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - prefer shorter hit (bug #20546)', function () { @@ -907,10 +907,10 @@ suite('Fuzzy Scorer', () => { let query = 'listview'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #12095)', function () { @@ -921,10 +921,10 @@ suite('Fuzzy Scorer', () => { let query = 'filesexplorerview.ts'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceA, resourceC, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - prefer case match (bug #96122)', function () { @@ -934,10 +934,10 @@ suite('Fuzzy Scorer', () => { let query = 'Lists.php'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - prefer shorter match (bug #103052) - foo bar', function () { @@ -946,12 +946,12 @@ suite('Fuzzy Scorer', () => { for (const query of ['foo bar', 'foobar']) { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); } }); @@ -961,12 +961,12 @@ suite('Fuzzy Scorer', () => { for (const query of ['payment model', 'paymentmodel']) { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); } }); @@ -976,12 +976,12 @@ suite('Fuzzy Scorer', () => { for (const query of ['color js', 'colorjs']) { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); } }); @@ -992,22 +992,22 @@ suite('Fuzzy Scorer', () => { let query = 'Color'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); query = 'color'; res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); }); test('compareFilesByScore - prefer prefix (bug #103052)', function () { @@ -1017,12 +1017,12 @@ suite('Fuzzy Scorer', () => { let query = 'smoke main.ts'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); }); test('compareFilesByScore - boost better prefix match if multiple queries are used', function () { @@ -1031,12 +1031,12 @@ suite('Fuzzy Scorer', () => { for (const query of ['workbench.ts browser', 'browser workbench.ts', 'browser workbench', 'workbench browser']) { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); } }); @@ -1046,12 +1046,12 @@ suite('Fuzzy Scorer', () => { for (const query of ['window browser', 'window.ts browser']) { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); } }); @@ -1061,73 +1061,73 @@ suite('Fuzzy Scorer', () => { for (const query of ['m life, life m']) { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); } }); test('prepareQuery', () => { - assert.equal(scorer.prepareQuery(' f*a ').normalized, 'fa'); - assert.equal(scorer.prepareQuery('model Tester.ts').original, 'model Tester.ts'); - assert.equal(scorer.prepareQuery('model Tester.ts').originalLowercase, 'model Tester.ts'.toLowerCase()); - assert.equal(scorer.prepareQuery('model Tester.ts').normalized, 'modelTester.ts'); - assert.equal(scorer.prepareQuery('Model Tester.ts').normalizedLowercase, 'modeltester.ts'); - assert.equal(scorer.prepareQuery('ModelTester.ts').containsPathSeparator, false); - assert.equal(scorer.prepareQuery('Model' + sep + 'Tester.ts').containsPathSeparator, true); + assert.strictEqual(scorer.prepareQuery(' f*a ').normalized, 'fa'); + assert.strictEqual(scorer.prepareQuery('model Tester.ts').original, 'model Tester.ts'); + assert.strictEqual(scorer.prepareQuery('model Tester.ts').originalLowercase, 'model Tester.ts'.toLowerCase()); + assert.strictEqual(scorer.prepareQuery('model Tester.ts').normalized, 'modelTester.ts'); + assert.strictEqual(scorer.prepareQuery('Model Tester.ts').normalizedLowercase, 'modeltester.ts'); + assert.strictEqual(scorer.prepareQuery('ModelTester.ts').containsPathSeparator, false); + assert.strictEqual(scorer.prepareQuery('Model' + sep + 'Tester.ts').containsPathSeparator, true); // with spaces let query = scorer.prepareQuery('He*llo World'); - assert.equal(query.original, 'He*llo World'); - assert.equal(query.normalized, 'HelloWorld'); - assert.equal(query.normalizedLowercase, 'HelloWorld'.toLowerCase()); - assert.equal(query.values?.length, 2); - assert.equal(query.values?.[0].original, 'He*llo'); - assert.equal(query.values?.[0].normalized, 'Hello'); - assert.equal(query.values?.[0].normalizedLowercase, 'Hello'.toLowerCase()); - assert.equal(query.values?.[1].original, 'World'); - assert.equal(query.values?.[1].normalized, 'World'); - assert.equal(query.values?.[1].normalizedLowercase, 'World'.toLowerCase()); + assert.strictEqual(query.original, 'He*llo World'); + assert.strictEqual(query.normalized, 'HelloWorld'); + assert.strictEqual(query.normalizedLowercase, 'HelloWorld'.toLowerCase()); + assert.strictEqual(query.values?.length, 2); + assert.strictEqual(query.values?.[0].original, 'He*llo'); + assert.strictEqual(query.values?.[0].normalized, 'Hello'); + assert.strictEqual(query.values?.[0].normalizedLowercase, 'Hello'.toLowerCase()); + assert.strictEqual(query.values?.[1].original, 'World'); + assert.strictEqual(query.values?.[1].normalized, 'World'); + assert.strictEqual(query.values?.[1].normalizedLowercase, 'World'.toLowerCase()); let restoredQuery = scorer.pieceToQuery(query.values!); - assert.equal(restoredQuery.original, query.original); - assert.equal(restoredQuery.values?.length, query.values?.length); - assert.equal(restoredQuery.containsPathSeparator, query.containsPathSeparator); + assert.strictEqual(restoredQuery.original, query.original); + assert.strictEqual(restoredQuery.values?.length, query.values?.length); + assert.strictEqual(restoredQuery.containsPathSeparator, query.containsPathSeparator); // with spaces that are empty query = scorer.prepareQuery(' Hello World '); - assert.equal(query.original, ' Hello World '); - assert.equal(query.originalLowercase, ' Hello World '.toLowerCase()); - assert.equal(query.normalized, 'HelloWorld'); - assert.equal(query.normalizedLowercase, 'HelloWorld'.toLowerCase()); - assert.equal(query.values?.length, 2); - assert.equal(query.values?.[0].original, 'Hello'); - assert.equal(query.values?.[0].originalLowercase, 'Hello'.toLowerCase()); - assert.equal(query.values?.[0].normalized, 'Hello'); - assert.equal(query.values?.[0].normalizedLowercase, 'Hello'.toLowerCase()); - assert.equal(query.values?.[1].original, 'World'); - assert.equal(query.values?.[1].originalLowercase, 'World'.toLowerCase()); - assert.equal(query.values?.[1].normalized, 'World'); - assert.equal(query.values?.[1].normalizedLowercase, 'World'.toLowerCase()); + assert.strictEqual(query.original, ' Hello World '); + assert.strictEqual(query.originalLowercase, ' Hello World '.toLowerCase()); + assert.strictEqual(query.normalized, 'HelloWorld'); + assert.strictEqual(query.normalizedLowercase, 'HelloWorld'.toLowerCase()); + assert.strictEqual(query.values?.length, 2); + assert.strictEqual(query.values?.[0].original, 'Hello'); + assert.strictEqual(query.values?.[0].originalLowercase, 'Hello'.toLowerCase()); + assert.strictEqual(query.values?.[0].normalized, 'Hello'); + assert.strictEqual(query.values?.[0].normalizedLowercase, 'Hello'.toLowerCase()); + assert.strictEqual(query.values?.[1].original, 'World'); + assert.strictEqual(query.values?.[1].originalLowercase, 'World'.toLowerCase()); + assert.strictEqual(query.values?.[1].normalized, 'World'); + assert.strictEqual(query.values?.[1].normalizedLowercase, 'World'.toLowerCase()); // Path related if (isWindows) { - assert.equal(scorer.prepareQuery('C:\\some\\path').pathNormalized, 'C:\\some\\path'); - assert.equal(scorer.prepareQuery('C:\\some\\path').normalized, 'C:\\some\\path'); - assert.equal(scorer.prepareQuery('C:\\some\\path').containsPathSeparator, true); - assert.equal(scorer.prepareQuery('C:/some/path').pathNormalized, 'C:\\some\\path'); - assert.equal(scorer.prepareQuery('C:/some/path').normalized, 'C:\\some\\path'); - assert.equal(scorer.prepareQuery('C:/some/path').containsPathSeparator, true); + assert.strictEqual(scorer.prepareQuery('C:\\some\\path').pathNormalized, 'C:\\some\\path'); + assert.strictEqual(scorer.prepareQuery('C:\\some\\path').normalized, 'C:\\some\\path'); + assert.strictEqual(scorer.prepareQuery('C:\\some\\path').containsPathSeparator, true); + assert.strictEqual(scorer.prepareQuery('C:/some/path').pathNormalized, 'C:\\some\\path'); + assert.strictEqual(scorer.prepareQuery('C:/some/path').normalized, 'C:\\some\\path'); + assert.strictEqual(scorer.prepareQuery('C:/some/path').containsPathSeparator, true); } else { - assert.equal(scorer.prepareQuery('/some/path').pathNormalized, '/some/path'); - assert.equal(scorer.prepareQuery('/some/path').normalized, '/some/path'); - assert.equal(scorer.prepareQuery('/some/path').containsPathSeparator, true); - assert.equal(scorer.prepareQuery('\\some\\path').pathNormalized, '/some/path'); - assert.equal(scorer.prepareQuery('\\some\\path').normalized, '/some/path'); - assert.equal(scorer.prepareQuery('\\some\\path').containsPathSeparator, true); + assert.strictEqual(scorer.prepareQuery('/some/path').pathNormalized, '/some/path'); + assert.strictEqual(scorer.prepareQuery('/some/path').normalized, '/some/path'); + assert.strictEqual(scorer.prepareQuery('/some/path').containsPathSeparator, true); + assert.strictEqual(scorer.prepareQuery('\\some\\path').pathNormalized, '/some/path'); + assert.strictEqual(scorer.prepareQuery('\\some\\path').normalized, '/some/path'); + assert.strictEqual(scorer.prepareQuery('\\some\\path').containsPathSeparator, true); } }); @@ -1138,18 +1138,18 @@ suite('Fuzzy Scorer', () => { let [score, matches] = _doScore2(offset === 0 ? target : `123${target}`, 'HeLlo-World', offset); assert.ok(score); - assert.equal(matches.length, 1); - assert.equal(matches[0].start, 0 + offset); - assert.equal(matches[0].end, target.length + offset); + assert.strictEqual(matches.length, 1); + assert.strictEqual(matches[0].start, 0 + offset); + assert.strictEqual(matches[0].end, target.length + offset); [score, matches] = _doScore2(offset === 0 ? target : `123${target}`, 'HW', offset); assert.ok(score); - assert.equal(matches.length, 2); - assert.equal(matches[0].start, 0 + offset); - assert.equal(matches[0].end, 1 + offset); - assert.equal(matches[1].start, 6 + offset); - assert.equal(matches[1].end, 7 + offset); + assert.strictEqual(matches.length, 2); + assert.strictEqual(matches[0].start, 0 + offset); + assert.strictEqual(matches[0].end, 1 + offset); + assert.strictEqual(matches[1].start, 6 + offset); + assert.strictEqual(matches[1].end, 7 + offset); } }); @@ -1169,8 +1169,8 @@ suite('Fuzzy Scorer', () => { const firstAndSecondSingleMatch = firstAndSecondSingleMatches[i]; if (multiMatch && firstAndSecondSingleMatch) { - assert.equal(multiMatch.start, firstAndSecondSingleMatch.start); - assert.equal(multiMatch.end, firstAndSecondSingleMatch.end); + assert.strictEqual(multiMatch.start, firstAndSecondSingleMatch.start); + assert.strictEqual(multiMatch.end, firstAndSecondSingleMatch.end); } else { assert.fail(); } @@ -1178,8 +1178,8 @@ suite('Fuzzy Scorer', () => { } function assertNoScore() { - assert.equal(multiScore, undefined); - assert.equal(multiMatches.length, 0); + assert.strictEqual(multiScore, undefined); + assert.strictEqual(multiMatches.length, 0); } assertScore(); diff --git a/src/vs/base/test/node/glob.test.ts b/src/vs/base/test/common/glob.test.ts similarity index 98% rename from src/vs/base/test/node/glob.test.ts rename to src/vs/base/test/common/glob.test.ts index 2c0288e3..25b83885 100644 --- a/src/vs/base/test/node/glob.test.ts +++ b/src/vs/base/test/common/glob.test.ts @@ -63,10 +63,12 @@ suite('Glob', () => { function assertGlobMatch(pattern: string | glob.IRelativePattern, input: string) { assert(glob.match(pattern, input), `${pattern} should match ${input}`); + assert(glob.match(pattern, nativeSep(input)), `${pattern} should match ${nativeSep(input)}`); } function assertNoGlobMatch(pattern: string | glob.IRelativePattern, input: string) { assert(!glob.match(pattern, input), `${pattern} should not match ${input}`); + assert(!glob.match(pattern, nativeSep(input)), `${pattern} should not match ${nativeSep(input)}`); } test('simple', () => { @@ -538,9 +540,13 @@ suite('Glob', () => { }); test('full path', function () { - let p = 'testing/this/foo.txt'; + assertGlobMatch('testing/this/foo.txt', 'testing/this/foo.txt'); + // assertGlobMatch('testing/this/foo.txt', 'testing\\this\\foo.txt'); + }); - assert(glob.match(p, nativeSep('testing/this/foo.txt'))); + test('ending path', function () { + assertGlobMatch('**/testing/this/foo.txt', 'some/path/testing/this/foo.txt'); + // assertGlobMatch('**/testing/this/foo.txt', 'some\\path\\testing\\this\\foo.txt'); }); test('prefix agnostic', function () { diff --git a/src/vs/base/test/common/iconLabels.test.ts b/src/vs/base/test/common/iconLabels.test.ts new file mode 100644 index 00000000..998c6897 --- /dev/null +++ b/src/vs/base/test/common/iconLabels.test.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { IMatch } from 'vs/base/common/filters'; +import { matchesFuzzyIconAware, parseLabelWithIcons, IParsedLabelWithIcons, stripIcons } from 'vs/base/common/iconLabels'; + +export interface IIconFilter { + // Returns null if word doesn't match. + (query: string, target: IParsedLabelWithIcons): IMatch[] | null; +} + +function filterOk(filter: IIconFilter, word: string, target: IParsedLabelWithIcons, highlights?: { start: number; end: number; }[]) { + let r = filter(word, target); + assert(r); + if (highlights) { + assert.deepEqual(r, highlights); + } +} + +suite('Icon Labels', () => { + test('matchesFuzzyIconAware', () => { + + // Camel Case + + filterOk(matchesFuzzyIconAware, 'ccr', parseLabelWithIcons('$(codicon)CamelCaseRocks$(codicon)'), [ + { start: 10, end: 11 }, + { start: 15, end: 16 }, + { start: 19, end: 20 } + ]); + + filterOk(matchesFuzzyIconAware, 'ccr', parseLabelWithIcons('$(codicon) CamelCaseRocks $(codicon)'), [ + { start: 11, end: 12 }, + { start: 16, end: 17 }, + { start: 20, end: 21 } + ]); + + filterOk(matchesFuzzyIconAware, 'iut', parseLabelWithIcons('$(codicon) Indent $(octico) Using $(octic) Tpaces'), [ + { start: 11, end: 12 }, + { start: 28, end: 29 }, + { start: 43, end: 44 }, + ]); + + // Prefix + + filterOk(matchesFuzzyIconAware, 'using', parseLabelWithIcons('$(codicon) Indent Using Spaces'), [ + { start: 18, end: 23 }, + ]); + + // Broken Codicon + + filterOk(matchesFuzzyIconAware, 'codicon', parseLabelWithIcons('This $(codicon Indent Using Spaces'), [ + { start: 7, end: 14 }, + ]); + + filterOk(matchesFuzzyIconAware, 'indent', parseLabelWithIcons('This $codicon Indent Using Spaces'), [ + { start: 14, end: 20 }, + ]); + + // Testing #59343 + filterOk(matchesFuzzyIconAware, 'unt', parseLabelWithIcons('$(primitive-dot) $(file-text) Untitled-1'), [ + { start: 30, end: 33 }, + ]); + }); + + test('stripIcons', () => { + assert.equal(stripIcons('Hello World'), 'Hello World'); + assert.equal(stripIcons('$(Hello World'), '$(Hello World'); + assert.equal(stripIcons('$(Hello) World'), ' World'); + assert.equal(stripIcons('$(Hello) W$(oi)rld'), ' Wrld'); + }); +}); diff --git a/src/vs/base/test/common/keyCodes.test.ts b/src/vs/base/test/common/keyCodes.test.ts index f8b2b55a..f5c5bcdc 100644 --- a/src/vs/base/test/common/keyCodes.test.ts +++ b/src/vs/base/test/common/keyCodes.test.ts @@ -10,7 +10,7 @@ import { OperatingSystem } from 'vs/base/common/platform'; suite('keyCodes', () => { function testBinaryEncoding(expected: Keybinding | null, k: number, OS: OperatingSystem): void { - assert.deepEqual(createKeybinding(k, OS), expected); + assert.deepStrictEqual(createKeybinding(k, OS), expected); } test('MAC binary encoding', () => { diff --git a/src/vs/base/test/common/labels.test.ts b/src/vs/base/test/common/labels.test.ts index a3e54c16..d7f0a401 100644 --- a/src/vs/base/test/common/labels.test.ts +++ b/src/vs/base/test/common/labels.test.ts @@ -8,106 +8,98 @@ import * as labels from 'vs/base/common/labels'; import * as platform from 'vs/base/common/platform'; suite('Labels', () => { - test('shorten - windows', () => { - if (!platform.isWindows) { - assert.ok(true); - return; - } + (!platform.isWindows ? test.skip : test)('shorten - windows', () => { // nothing to shorten - assert.deepEqual(labels.shorten(['a']), ['a']); - assert.deepEqual(labels.shorten(['a', 'b']), ['a', 'b']); - assert.deepEqual(labels.shorten(['a', 'b', 'c']), ['a', 'b', 'c']); + assert.deepStrictEqual(labels.shorten(['a']), ['a']); + assert.deepStrictEqual(labels.shorten(['a', 'b']), ['a', 'b']); + assert.deepStrictEqual(labels.shorten(['a', 'b', 'c']), ['a', 'b', 'c']); // completely different paths - assert.deepEqual(labels.shorten(['a\\b', 'c\\d', 'e\\f']), ['…\\b', '…\\d', '…\\f']); + assert.deepStrictEqual(labels.shorten(['a\\b', 'c\\d', 'e\\f']), ['…\\b', '…\\d', '…\\f']); // same beginning - assert.deepEqual(labels.shorten(['a', 'a\\b']), ['a', '…\\b']); - assert.deepEqual(labels.shorten(['a\\b', 'a\\b\\c']), ['…\\b', '…\\c']); - assert.deepEqual(labels.shorten(['a', 'a\\b', 'a\\b\\c']), ['a', '…\\b', '…\\c']); - assert.deepEqual(labels.shorten(['x:\\a\\b', 'x:\\a\\c']), ['x:\\…\\b', 'x:\\…\\c']); - assert.deepEqual(labels.shorten(['\\\\a\\b', '\\\\a\\c']), ['\\\\a\\b', '\\\\a\\c']); + assert.deepStrictEqual(labels.shorten(['a', 'a\\b']), ['a', '…\\b']); + assert.deepStrictEqual(labels.shorten(['a\\b', 'a\\b\\c']), ['…\\b', '…\\c']); + assert.deepStrictEqual(labels.shorten(['a', 'a\\b', 'a\\b\\c']), ['a', '…\\b', '…\\c']); + assert.deepStrictEqual(labels.shorten(['x:\\a\\b', 'x:\\a\\c']), ['x:\\…\\b', 'x:\\…\\c']); + assert.deepStrictEqual(labels.shorten(['\\\\a\\b', '\\\\a\\c']), ['\\\\a\\b', '\\\\a\\c']); // same ending - assert.deepEqual(labels.shorten(['a', 'b\\a']), ['a', 'b\\…']); - assert.deepEqual(labels.shorten(['a\\b\\c', 'd\\b\\c']), ['a\\…', 'd\\…']); - assert.deepEqual(labels.shorten(['a\\b\\c\\d', 'f\\b\\c\\d']), ['a\\…', 'f\\…']); - assert.deepEqual(labels.shorten(['d\\e\\a\\b\\c', 'd\\b\\c']), ['…\\a\\…', 'd\\b\\…']); - assert.deepEqual(labels.shorten(['a\\b\\c\\d', 'a\\f\\b\\c\\d']), ['a\\b\\…', '…\\f\\…']); - assert.deepEqual(labels.shorten(['a\\b\\a', 'b\\b\\a']), ['a\\b\\…', 'b\\b\\…']); - assert.deepEqual(labels.shorten(['d\\f\\a\\b\\c', 'h\\d\\b\\c']), ['…\\a\\…', 'h\\…']); - assert.deepEqual(labels.shorten(['a\\b\\c', 'x:\\0\\a\\b\\c']), ['a\\b\\c', 'x:\\0\\…']); - assert.deepEqual(labels.shorten(['x:\\a\\b\\c', 'x:\\0\\a\\b\\c']), ['x:\\a\\…', 'x:\\0\\…']); - assert.deepEqual(labels.shorten(['x:\\a\\b', 'y:\\a\\b']), ['x:\\…', 'y:\\…']); - assert.deepEqual(labels.shorten(['x:\\a', 'x:\\c']), ['x:\\a', 'x:\\c']); - assert.deepEqual(labels.shorten(['x:\\a\\b', 'y:\\x\\a\\b']), ['x:\\…', 'y:\\…']); - assert.deepEqual(labels.shorten(['\\\\x\\b', '\\\\y\\b']), ['\\\\x\\…', '\\\\y\\…']); - assert.deepEqual(labels.shorten(['\\\\x\\a', '\\\\x\\b']), ['\\\\x\\a', '\\\\x\\b']); + assert.deepStrictEqual(labels.shorten(['a', 'b\\a']), ['a', 'b\\…']); + assert.deepStrictEqual(labels.shorten(['a\\b\\c', 'd\\b\\c']), ['a\\…', 'd\\…']); + assert.deepStrictEqual(labels.shorten(['a\\b\\c\\d', 'f\\b\\c\\d']), ['a\\…', 'f\\…']); + assert.deepStrictEqual(labels.shorten(['d\\e\\a\\b\\c', 'd\\b\\c']), ['…\\a\\…', 'd\\b\\…']); + assert.deepStrictEqual(labels.shorten(['a\\b\\c\\d', 'a\\f\\b\\c\\d']), ['a\\b\\…', '…\\f\\…']); + assert.deepStrictEqual(labels.shorten(['a\\b\\a', 'b\\b\\a']), ['a\\b\\…', 'b\\b\\…']); + assert.deepStrictEqual(labels.shorten(['d\\f\\a\\b\\c', 'h\\d\\b\\c']), ['…\\a\\…', 'h\\…']); + assert.deepStrictEqual(labels.shorten(['a\\b\\c', 'x:\\0\\a\\b\\c']), ['a\\b\\c', 'x:\\0\\…']); + assert.deepStrictEqual(labels.shorten(['x:\\a\\b\\c', 'x:\\0\\a\\b\\c']), ['x:\\a\\…', 'x:\\0\\…']); + assert.deepStrictEqual(labels.shorten(['x:\\a\\b', 'y:\\a\\b']), ['x:\\…', 'y:\\…']); + assert.deepStrictEqual(labels.shorten(['x:\\a', 'x:\\c']), ['x:\\a', 'x:\\c']); + assert.deepStrictEqual(labels.shorten(['x:\\a\\b', 'y:\\x\\a\\b']), ['x:\\…', 'y:\\…']); + assert.deepStrictEqual(labels.shorten(['\\\\x\\b', '\\\\y\\b']), ['\\\\x\\…', '\\\\y\\…']); + assert.deepStrictEqual(labels.shorten(['\\\\x\\a', '\\\\x\\b']), ['\\\\x\\a', '\\\\x\\b']); // same name ending - assert.deepEqual(labels.shorten(['a\\b', 'a\\c', 'a\\e-b']), ['…\\b', '…\\c', '…\\e-b']); + assert.deepStrictEqual(labels.shorten(['a\\b', 'a\\c', 'a\\e-b']), ['…\\b', '…\\c', '…\\e-b']); // same in the middle - assert.deepEqual(labels.shorten(['a\\b\\c', 'd\\b\\e']), ['…\\c', '…\\e']); + assert.deepStrictEqual(labels.shorten(['a\\b\\c', 'd\\b\\e']), ['…\\c', '…\\e']); // case-sensetive - assert.deepEqual(labels.shorten(['a\\b\\c', 'd\\b\\C']), ['…\\c', '…\\C']); + assert.deepStrictEqual(labels.shorten(['a\\b\\c', 'd\\b\\C']), ['…\\c', '…\\C']); // empty or null - assert.deepEqual(labels.shorten(['', null!]), ['.\\', null]); + assert.deepStrictEqual(labels.shorten(['', null!]), ['.\\', null]); - assert.deepEqual(labels.shorten(['a', 'a\\b', 'a\\b\\c', 'd\\b\\c', 'd\\b']), ['a', 'a\\b', 'a\\b\\c', 'd\\b\\c', 'd\\b']); - assert.deepEqual(labels.shorten(['a', 'a\\b', 'b']), ['a', 'a\\b', 'b']); - assert.deepEqual(labels.shorten(['', 'a', 'b', 'b\\c', 'a\\c']), ['.\\', 'a', 'b', 'b\\c', 'a\\c']); - assert.deepEqual(labels.shorten(['src\\vs\\workbench\\parts\\execution\\electron-browser', 'src\\vs\\workbench\\parts\\execution\\electron-browser\\something', 'src\\vs\\workbench\\parts\\terminal\\electron-browser']), ['…\\execution\\electron-browser', '…\\something', '…\\terminal\\…']); + assert.deepStrictEqual(labels.shorten(['a', 'a\\b', 'a\\b\\c', 'd\\b\\c', 'd\\b']), ['a', 'a\\b', 'a\\b\\c', 'd\\b\\c', 'd\\b']); + assert.deepStrictEqual(labels.shorten(['a', 'a\\b', 'b']), ['a', 'a\\b', 'b']); + assert.deepStrictEqual(labels.shorten(['', 'a', 'b', 'b\\c', 'a\\c']), ['.\\', 'a', 'b', 'b\\c', 'a\\c']); + assert.deepStrictEqual(labels.shorten(['src\\vs\\workbench\\parts\\execution\\electron-browser', 'src\\vs\\workbench\\parts\\execution\\electron-browser\\something', 'src\\vs\\workbench\\parts\\terminal\\electron-browser']), ['…\\execution\\electron-browser', '…\\something', '…\\terminal\\…']); }); - test('shorten - not windows', () => { - if (platform.isWindows) { - assert.ok(true); - return; - } + (platform.isWindows ? test.skip : test)('shorten - not windows', () => { // nothing to shorten - assert.deepEqual(labels.shorten(['a']), ['a']); - assert.deepEqual(labels.shorten(['a', 'b']), ['a', 'b']); - assert.deepEqual(labels.shorten(['a', 'b', 'c']), ['a', 'b', 'c']); + assert.deepStrictEqual(labels.shorten(['a']), ['a']); + assert.deepStrictEqual(labels.shorten(['a', 'b']), ['a', 'b']); + assert.deepStrictEqual(labels.shorten(['a', 'b', 'c']), ['a', 'b', 'c']); // completely different paths - assert.deepEqual(labels.shorten(['a/b', 'c/d', 'e/f']), ['…/b', '…/d', '…/f']); + assert.deepStrictEqual(labels.shorten(['a/b', 'c/d', 'e/f']), ['…/b', '…/d', '…/f']); // same beginning - assert.deepEqual(labels.shorten(['a', 'a/b']), ['a', '…/b']); - assert.deepEqual(labels.shorten(['a/b', 'a/b/c']), ['…/b', '…/c']); - assert.deepEqual(labels.shorten(['a', 'a/b', 'a/b/c']), ['a', '…/b', '…/c']); - assert.deepEqual(labels.shorten(['/a/b', '/a/c']), ['/a/b', '/a/c']); + assert.deepStrictEqual(labels.shorten(['a', 'a/b']), ['a', '…/b']); + assert.deepStrictEqual(labels.shorten(['a/b', 'a/b/c']), ['…/b', '…/c']); + assert.deepStrictEqual(labels.shorten(['a', 'a/b', 'a/b/c']), ['a', '…/b', '…/c']); + assert.deepStrictEqual(labels.shorten(['/a/b', '/a/c']), ['/a/b', '/a/c']); // same ending - assert.deepEqual(labels.shorten(['a', 'b/a']), ['a', 'b/…']); - assert.deepEqual(labels.shorten(['a/b/c', 'd/b/c']), ['a/…', 'd/…']); - assert.deepEqual(labels.shorten(['a/b/c/d', 'f/b/c/d']), ['a/…', 'f/…']); - assert.deepEqual(labels.shorten(['d/e/a/b/c', 'd/b/c']), ['…/a/…', 'd/b/…']); - assert.deepEqual(labels.shorten(['a/b/c/d', 'a/f/b/c/d']), ['a/b/…', '…/f/…']); - assert.deepEqual(labels.shorten(['a/b/a', 'b/b/a']), ['a/b/…', 'b/b/…']); - assert.deepEqual(labels.shorten(['d/f/a/b/c', 'h/d/b/c']), ['…/a/…', 'h/…']); - assert.deepEqual(labels.shorten(['/x/b', '/y/b']), ['/x/…', '/y/…']); + assert.deepStrictEqual(labels.shorten(['a', 'b/a']), ['a', 'b/…']); + assert.deepStrictEqual(labels.shorten(['a/b/c', 'd/b/c']), ['a/…', 'd/…']); + assert.deepStrictEqual(labels.shorten(['a/b/c/d', 'f/b/c/d']), ['a/…', 'f/…']); + assert.deepStrictEqual(labels.shorten(['d/e/a/b/c', 'd/b/c']), ['…/a/…', 'd/b/…']); + assert.deepStrictEqual(labels.shorten(['a/b/c/d', 'a/f/b/c/d']), ['a/b/…', '…/f/…']); + assert.deepStrictEqual(labels.shorten(['a/b/a', 'b/b/a']), ['a/b/…', 'b/b/…']); + assert.deepStrictEqual(labels.shorten(['d/f/a/b/c', 'h/d/b/c']), ['…/a/…', 'h/…']); + assert.deepStrictEqual(labels.shorten(['/x/b', '/y/b']), ['/x/…', '/y/…']); // same name ending - assert.deepEqual(labels.shorten(['a/b', 'a/c', 'a/e-b']), ['…/b', '…/c', '…/e-b']); + assert.deepStrictEqual(labels.shorten(['a/b', 'a/c', 'a/e-b']), ['…/b', '…/c', '…/e-b']); // same in the middle - assert.deepEqual(labels.shorten(['a/b/c', 'd/b/e']), ['…/c', '…/e']); + assert.deepStrictEqual(labels.shorten(['a/b/c', 'd/b/e']), ['…/c', '…/e']); // case-sensitive - assert.deepEqual(labels.shorten(['a/b/c', 'd/b/C']), ['…/c', '…/C']); + assert.deepStrictEqual(labels.shorten(['a/b/c', 'd/b/C']), ['…/c', '…/C']); // empty or null - assert.deepEqual(labels.shorten(['', null!]), ['./', null]); + assert.deepStrictEqual(labels.shorten(['', null!]), ['./', null]); - assert.deepEqual(labels.shorten(['a', 'a/b', 'a/b/c', 'd/b/c', 'd/b']), ['a', 'a/b', 'a/b/c', 'd/b/c', 'd/b']); - assert.deepEqual(labels.shorten(['a', 'a/b', 'b']), ['a', 'a/b', 'b']); - assert.deepEqual(labels.shorten(['', 'a', 'b', 'b/c', 'a/c']), ['./', 'a', 'b', 'b/c', 'a/c']); + assert.deepStrictEqual(labels.shorten(['a', 'a/b', 'a/b/c', 'd/b/c', 'd/b']), ['a', 'a/b', 'a/b/c', 'd/b/c', 'd/b']); + assert.deepStrictEqual(labels.shorten(['a', 'a/b', 'b']), ['a', 'a/b', 'b']); + assert.deepStrictEqual(labels.shorten(['', 'a', 'b', 'b/c', 'a/c']), ['./', 'a', 'b', 'b/c', 'a/c']); }); test('template', () => { @@ -142,41 +134,32 @@ suite('Labels', () => { assert.strictEqual(labels.template(t, { dirty: '* ', activeEditorShort: 'somefile.txt', rootName: 'monaco', appName: 'Visual Studio Code', separator: { label: ' - ' } }), '* somefile.txt - monaco - Visual Studio Code'); }); - test('getBaseLabel - unix', () => { - if (platform.isWindows) { - assert.ok(true); - return; - } - - assert.equal(labels.getBaseLabel('/some/folder/file.txt'), 'file.txt'); - assert.equal(labels.getBaseLabel('/some/folder'), 'folder'); - assert.equal(labels.getBaseLabel('/'), '/'); + (platform.isWindows ? test.skip : test)('getBaseLabel - unix', () => { + assert.strictEqual(labels.getBaseLabel('/some/folder/file.txt'), 'file.txt'); + assert.strictEqual(labels.getBaseLabel('/some/folder'), 'folder'); + assert.strictEqual(labels.getBaseLabel('/'), '/'); }); - test('getBaseLabel - windows', () => { - if (!platform.isWindows) { - assert.ok(true); - return; - } - - assert.equal(labels.getBaseLabel('c:'), 'C:'); - assert.equal(labels.getBaseLabel('c:\\'), 'C:'); - assert.equal(labels.getBaseLabel('c:\\some\\folder\\file.txt'), 'file.txt'); - assert.equal(labels.getBaseLabel('c:\\some\\folder'), 'folder'); + (!platform.isWindows ? test.skip : test)('getBaseLabel - windows', () => { + assert.strictEqual(labels.getBaseLabel('c:'), 'C:'); + assert.strictEqual(labels.getBaseLabel('c:\\'), 'C:'); + assert.strictEqual(labels.getBaseLabel('c:\\some\\folder\\file.txt'), 'file.txt'); + assert.strictEqual(labels.getBaseLabel('c:\\some\\folder'), 'folder'); + assert.strictEqual(labels.getBaseLabel('c:\\some\\f:older'), 'f:older'); // https://github.com/microsoft/vscode-remote-release/issues/4227 }); test('mnemonicButtonLabel', () => { - assert.equal(labels.mnemonicButtonLabel('Hello World'), 'Hello World'); - assert.equal(labels.mnemonicButtonLabel(''), ''); + assert.strictEqual(labels.mnemonicButtonLabel('Hello World'), 'Hello World'); + assert.strictEqual(labels.mnemonicButtonLabel(''), ''); if (platform.isWindows) { - assert.equal(labels.mnemonicButtonLabel('Hello & World'), 'Hello && World'); - assert.equal(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do ¬ Save && Continue'); + assert.strictEqual(labels.mnemonicButtonLabel('Hello & World'), 'Hello && World'); + assert.strictEqual(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do ¬ Save && Continue'); } else if (platform.isMacintosh) { - assert.equal(labels.mnemonicButtonLabel('Hello & World'), 'Hello & World'); - assert.equal(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do not Save & Continue'); + assert.strictEqual(labels.mnemonicButtonLabel('Hello & World'), 'Hello & World'); + assert.strictEqual(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do not Save & Continue'); } else { - assert.equal(labels.mnemonicButtonLabel('Hello & World'), 'Hello & World'); - assert.equal(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do _not Save & Continue'); + assert.strictEqual(labels.mnemonicButtonLabel('Hello & World'), 'Hello & World'); + assert.strictEqual(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do _not Save & Continue'); } }); -}); \ No newline at end of file +}); diff --git a/src/vs/base/test/common/linkedList.test.ts b/src/vs/base/test/common/linkedList.test.ts index dbb057a2..d4eb0832 100644 --- a/src/vs/base/test/common/linkedList.test.ts +++ b/src/vs/base/test/common/linkedList.test.ts @@ -11,19 +11,19 @@ suite('LinkedList', function () { function assertElements(list: LinkedList, ...elements: E[]) { // check size - assert.equal(list.size, elements.length); + assert.strictEqual(list.size, elements.length); // assert toArray - assert.deepEqual(Array.from(list), elements); + assert.deepStrictEqual(Array.from(list), elements); // assert Symbol.iterator (1) - assert.deepEqual([...list], elements); + assert.deepStrictEqual([...list], elements); // assert Symbol.iterator (2) for (const item of list) { - assert.equal(item, elements.shift()); + assert.strictEqual(item, elements.shift()); } - assert.equal(elements.length, 0); + assert.strictEqual(elements.length, 0); } test('Push/Iter', () => { @@ -123,14 +123,14 @@ suite('LinkedList', function () { assertElements(list, 'a', 'b'); let a = list.shift(); - assert.equal(a, 'a'); + assert.strictEqual(a, 'a'); assertElements(list, 'b'); list.unshift('a'); assertElements(list, 'a', 'b'); let b = list.pop(); - assert.equal(b, 'b'); + assert.strictEqual(b, 'b'); assertElements(list, 'a'); }); diff --git a/src/vs/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index 5781de52..8988e8e6 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -16,8 +16,8 @@ suite('Map', () => { map.set('bk', 'bv'); assert.deepStrictEqual([...map.keys()], ['ak', 'bk']); assert.deepStrictEqual([...map.values()], ['av', 'bv']); - assert.equal(map.first, 'av'); - assert.equal(map.last, 'bv'); + assert.strictEqual(map.first, 'av'); + assert.strictEqual(map.last, 'bv'); }); test('LinkedMap - Touch Old one', () => { @@ -77,7 +77,7 @@ suite('Map', () => { test('LinkedMap - basics', function () { const map = new LinkedMap(); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); map.set('1', 1); map.set('2', '2'); @@ -89,23 +89,23 @@ suite('Map', () => { const date = Date.now(); map.set('5', date); - assert.equal(map.size, 5); - assert.equal(map.get('1'), 1); - assert.equal(map.get('2'), '2'); - assert.equal(map.get('3'), true); - assert.equal(map.get('4'), obj); - assert.equal(map.get('5'), date); + assert.strictEqual(map.size, 5); + assert.strictEqual(map.get('1'), 1); + assert.strictEqual(map.get('2'), '2'); + assert.strictEqual(map.get('3'), true); + assert.strictEqual(map.get('4'), obj); + assert.strictEqual(map.get('5'), date); assert.ok(!map.get('6')); map.delete('6'); - assert.equal(map.size, 5); - assert.equal(map.delete('1'), true); - assert.equal(map.delete('2'), true); - assert.equal(map.delete('3'), true); - assert.equal(map.delete('4'), true); - assert.equal(map.delete('5'), true); + assert.strictEqual(map.size, 5); + assert.strictEqual(map.delete('1'), true); + assert.strictEqual(map.delete('2'), true); + assert.strictEqual(map.delete('3'), true); + assert.strictEqual(map.delete('4'), true); + assert.strictEqual(map.delete('5'), true); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); assert.ok(!map.get('5')); assert.ok(!map.get('4')); assert.ok(!map.get('3')); @@ -117,13 +117,13 @@ suite('Map', () => { map.set('3', true); assert.ok(map.has('1')); - assert.equal(map.get('1'), 1); - assert.equal(map.get('2'), '2'); - assert.equal(map.get('3'), true); + assert.strictEqual(map.get('1'), 1); + assert.strictEqual(map.get('2'), '2'); + assert.strictEqual(map.get('3'), true); map.clear(); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); assert.ok(!map.get('1')); assert.ok(!map.get('2')); assert.ok(!map.get('3')); @@ -231,7 +231,7 @@ suite('Map', () => { for (let i = 11; i <= 20; i++) { cache.set(i, i); } - assert.deepEqual(cache.size, 15); + assert.deepStrictEqual(cache.size, 15); let values: number[] = []; for (let i = 6; i <= 20; i++) { values.push(cache.get(i)!); @@ -269,14 +269,14 @@ suite('Map', () => { let i = 0; map.forEach((value, key) => { if (i === 0) { - assert.equal(key, 'ak'); - assert.equal(value, 'av'); + assert.strictEqual(key, 'ak'); + assert.strictEqual(value, 'av'); } else if (i === 1) { - assert.equal(key, 'bk'); - assert.equal(value, 'bv'); + assert.strictEqual(key, 'bk'); + assert.strictEqual(value, 'bv'); } else if (i === 2) { - assert.equal(key, 'ck'); - assert.equal(value, 'cv'); + assert.strictEqual(key, 'ck'); + assert.strictEqual(value, 'cv'); } i++; }); @@ -285,44 +285,44 @@ suite('Map', () => { test('LinkedMap - delete Head and Tail', function () { const map = new LinkedMap(); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); map.set('1', 1); - assert.equal(map.size, 1); + assert.strictEqual(map.size, 1); map.delete('1'); - assert.equal(map.get('1'), undefined); - assert.equal(map.size, 0); - assert.equal([...map.keys()].length, 0); + assert.strictEqual(map.get('1'), undefined); + assert.strictEqual(map.size, 0); + assert.strictEqual([...map.keys()].length, 0); }); test('LinkedMap - delete Head', function () { const map = new LinkedMap(); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); map.set('1', 1); map.set('2', 2); - assert.equal(map.size, 2); + assert.strictEqual(map.size, 2); map.delete('1'); - assert.equal(map.get('2'), 2); - assert.equal(map.size, 1); - assert.equal([...map.keys()].length, 1); - assert.equal([...map.keys()][0], 2); + assert.strictEqual(map.get('2'), 2); + assert.strictEqual(map.size, 1); + assert.strictEqual([...map.keys()].length, 1); + assert.strictEqual([...map.keys()][0], '2'); }); test('LinkedMap - delete Tail', function () { const map = new LinkedMap(); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); map.set('1', 1); map.set('2', 2); - assert.equal(map.size, 2); + assert.strictEqual(map.size, 2); map.delete('2'); - assert.equal(map.get('1'), 1); - assert.equal(map.size, 1); - assert.equal([...map.keys()].length, 1); - assert.equal([...map.keys()][0], 1); + assert.strictEqual(map.get('1'), 1); + assert.strictEqual(map.size, 1); + assert.strictEqual([...map.keys()].length, 1); + assert.strictEqual([...map.keys()][0], '1'); }); @@ -330,100 +330,100 @@ suite('Map', () => { const iter = new PathIterator(); iter.reset('file:///usr/bin/file.txt'); - assert.equal(iter.value(), 'file:'); - assert.equal(iter.hasNext(), true); - assert.equal(iter.cmp('file:'), 0); + assert.strictEqual(iter.value(), 'file:'); + assert.strictEqual(iter.hasNext(), true); + assert.strictEqual(iter.cmp('file:'), 0); assert.ok(iter.cmp('a') < 0); assert.ok(iter.cmp('aile:') < 0); assert.ok(iter.cmp('z') > 0); assert.ok(iter.cmp('zile:') > 0); iter.next(); - assert.equal(iter.value(), 'usr'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'usr'); + assert.strictEqual(iter.hasNext(), true); iter.next(); - assert.equal(iter.value(), 'bin'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'bin'); + assert.strictEqual(iter.hasNext(), true); iter.next(); - assert.equal(iter.value(), 'file.txt'); - assert.equal(iter.hasNext(), false); + assert.strictEqual(iter.value(), 'file.txt'); + assert.strictEqual(iter.hasNext(), false); iter.next(); - assert.equal(iter.value(), ''); - assert.equal(iter.hasNext(), false); + assert.strictEqual(iter.value(), ''); + assert.strictEqual(iter.hasNext(), false); iter.next(); - assert.equal(iter.value(), ''); - assert.equal(iter.hasNext(), false); + assert.strictEqual(iter.value(), ''); + assert.strictEqual(iter.hasNext(), false); // iter.reset('/foo/bar/'); - assert.equal(iter.value(), 'foo'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'foo'); + assert.strictEqual(iter.hasNext(), true); iter.next(); - assert.equal(iter.value(), 'bar'); - assert.equal(iter.hasNext(), false); + assert.strictEqual(iter.value(), 'bar'); + assert.strictEqual(iter.hasNext(), false); }); test('URIIterator', function () { const iter = new UriIterator(() => false); iter.reset(URI.parse('file:///usr/bin/file.txt')); - assert.equal(iter.value(), 'file'); - // assert.equal(iter.cmp('FILE'), 0); - assert.equal(iter.cmp('file'), 0); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'file'); + // assert.strictEqual(iter.cmp('FILE'), 0); + assert.strictEqual(iter.cmp('file'), 0); + assert.strictEqual(iter.hasNext(), true); iter.next(); - assert.equal(iter.value(), 'usr'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'usr'); + assert.strictEqual(iter.hasNext(), true); iter.next(); - assert.equal(iter.value(), 'bin'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'bin'); + assert.strictEqual(iter.hasNext(), true); iter.next(); - assert.equal(iter.value(), 'file.txt'); - assert.equal(iter.hasNext(), false); + assert.strictEqual(iter.value(), 'file.txt'); + assert.strictEqual(iter.hasNext(), false); iter.reset(URI.parse('file://share/usr/bin/file.txt?foo')); // scheme - assert.equal(iter.value(), 'file'); - // assert.equal(iter.cmp('FILE'), 0); - assert.equal(iter.cmp('file'), 0); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'file'); + // assert.strictEqual(iter.cmp('FILE'), 0); + assert.strictEqual(iter.cmp('file'), 0); + assert.strictEqual(iter.hasNext(), true); iter.next(); // authority - assert.equal(iter.value(), 'share'); - assert.equal(iter.cmp('SHARe'), 0); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'share'); + assert.strictEqual(iter.cmp('SHARe'), 0); + assert.strictEqual(iter.hasNext(), true); iter.next(); // path - assert.equal(iter.value(), 'usr'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'usr'); + assert.strictEqual(iter.hasNext(), true); iter.next(); // path - assert.equal(iter.value(), 'bin'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'bin'); + assert.strictEqual(iter.hasNext(), true); iter.next(); // path - assert.equal(iter.value(), 'file.txt'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'file.txt'); + assert.strictEqual(iter.hasNext(), true); iter.next(); // query - assert.equal(iter.value(), 'foo'); - assert.equal(iter.cmp('z') > 0, true); - assert.equal(iter.cmp('a') < 0, true); - assert.equal(iter.hasNext(), false); + assert.strictEqual(iter.value(), 'foo'); + assert.strictEqual(iter.cmp('z') > 0, true); + assert.strictEqual(iter.cmp('a') < 0, true); + assert.strictEqual(iter.hasNext(), false); }); function assertTernarySearchTree(trie: TernarySearchTree, ...elements: [string, E][]) { @@ -432,24 +432,24 @@ suite('Map', () => { map.set(key, value); } map.forEach((value, key) => { - assert.equal(trie.get(key), value); + assert.strictEqual(trie.get(key), value); }); // forEach let forEachCount = 0; trie.forEach((element, key) => { - assert.equal(element, map.get(key)); + assert.strictEqual(element, map.get(key)); forEachCount++; }); - assert.equal(map.size, forEachCount); + assert.strictEqual(map.size, forEachCount); // iterator let iterCount = 0; for (let [key, value] of trie) { - assert.equal(value, map.get(key)); + assert.strictEqual(value, map.get(key)); iterCount++; } - assert.equal(map.size, iterCount); + assert.strictEqual(map.size, iterCount); } test('TernarySearchTree - set', function () { @@ -493,13 +493,13 @@ suite('Map', () => { trie.set('foobar', 2); trie.set('foobaz', 3); - assert.equal(trie.findSubstr('f'), undefined); - assert.equal(trie.findSubstr('z'), undefined); - assert.equal(trie.findSubstr('foo'), 1); - assert.equal(trie.findSubstr('fooö'), 1); - assert.equal(trie.findSubstr('fooba'), 1); - assert.equal(trie.findSubstr('foobarr'), 2); - assert.equal(trie.findSubstr('foobazrr'), 3); + assert.strictEqual(trie.findSubstr('f'), undefined); + assert.strictEqual(trie.findSubstr('z'), undefined); + assert.strictEqual(trie.findSubstr('foo'), 1); + assert.strictEqual(trie.findSubstr('fooö'), 1); + assert.strictEqual(trie.findSubstr('fooba'), 1); + assert.strictEqual(trie.findSubstr('foobarr'), 2); + assert.strictEqual(trie.findSubstr('foobazrr'), 3); }); test('TernarySearchTree - basics', function () { @@ -509,27 +509,27 @@ suite('Map', () => { trie.set('bar', 2); trie.set('foobar', 3); - assert.equal(trie.get('foo'), 1); - assert.equal(trie.get('bar'), 2); - assert.equal(trie.get('foobar'), 3); - assert.equal(trie.get('foobaz'), undefined); - assert.equal(trie.get('foobarr'), undefined); + assert.strictEqual(trie.get('foo'), 1); + assert.strictEqual(trie.get('bar'), 2); + assert.strictEqual(trie.get('foobar'), 3); + assert.strictEqual(trie.get('foobaz'), undefined); + assert.strictEqual(trie.get('foobarr'), undefined); - assert.equal(trie.findSubstr('fo'), undefined); - assert.equal(trie.findSubstr('foo'), 1); - assert.equal(trie.findSubstr('foooo'), 1); + assert.strictEqual(trie.findSubstr('fo'), undefined); + assert.strictEqual(trie.findSubstr('foo'), 1); + assert.strictEqual(trie.findSubstr('foooo'), 1); trie.delete('foobar'); trie.delete('bar'); - assert.equal(trie.get('foobar'), undefined); - assert.equal(trie.get('bar'), undefined); + assert.strictEqual(trie.get('foobar'), undefined); + assert.strictEqual(trie.get('bar'), undefined); trie.set('foobar', 17); trie.set('barr', 18); - assert.equal(trie.get('foobar'), 17); - assert.equal(trie.get('barr'), 18); - assert.equal(trie.get('bar'), undefined); + assert.strictEqual(trie.get('foobar'), 17); + assert.strictEqual(trie.get('barr'), 18); + assert.strictEqual(trie.get('bar'), undefined); }); test('TernarySearchTree - delete & cleanup', function () { @@ -576,20 +576,20 @@ suite('Map', () => { trie.set('/user/foo', 2); trie.set('/user/foo/flip/flop', 3); - assert.equal(trie.get('/user/foo/bar'), 1); - assert.equal(trie.get('/user/foo'), 2); - assert.equal(trie.get('/user//foo'), 2); - assert.equal(trie.get('/user\\foo'), 2); - assert.equal(trie.get('/user/foo/flip/flop'), 3); + assert.strictEqual(trie.get('/user/foo/bar'), 1); + assert.strictEqual(trie.get('/user/foo'), 2); + assert.strictEqual(trie.get('/user//foo'), 2); + assert.strictEqual(trie.get('/user\\foo'), 2); + assert.strictEqual(trie.get('/user/foo/flip/flop'), 3); - assert.equal(trie.findSubstr('/user/bar'), undefined); - assert.equal(trie.findSubstr('/user/foo'), 2); - assert.equal(trie.findSubstr('\\user\\foo'), 2); - assert.equal(trie.findSubstr('/user//foo'), 2); - assert.equal(trie.findSubstr('/user/foo/ba'), 2); - assert.equal(trie.findSubstr('/user/foo/far/boo'), 2); - assert.equal(trie.findSubstr('/user/foo/bar'), 1); - assert.equal(trie.findSubstr('/user/foo/bar/far/boo'), 1); + assert.strictEqual(trie.findSubstr('/user/bar'), undefined); + assert.strictEqual(trie.findSubstr('/user/foo'), 2); + assert.strictEqual(trie.findSubstr('\\user\\foo'), 2); + assert.strictEqual(trie.findSubstr('/user//foo'), 2); + assert.strictEqual(trie.findSubstr('/user/foo/ba'), 2); + assert.strictEqual(trie.findSubstr('/user/foo/far/boo'), 2); + assert.strictEqual(trie.findSubstr('/user/foo/bar'), 1); + assert.strictEqual(trie.findSubstr('/user/foo/bar/far/boo'), 1); }); test('TernarySearchTree (PathSegments) - lookup', function () { @@ -599,11 +599,11 @@ suite('Map', () => { map.set('/user/foo', 2); map.set('/user/foo/flip/flop', 3); - assert.equal(map.get('/foo'), undefined); - assert.equal(map.get('/user'), undefined); - assert.equal(map.get('/user/foo'), 2); - assert.equal(map.get('/user/foo/bar'), 1); - assert.equal(map.get('/user/foo/bar/boo'), undefined); + assert.strictEqual(map.get('/foo'), undefined); + assert.strictEqual(map.get('/user'), undefined); + assert.strictEqual(map.get('/user/foo'), 2); + assert.strictEqual(map.get('/user/foo/bar'), 1); + assert.strictEqual(map.get('/user/foo/bar/boo'), undefined); }); test('TernarySearchTree (PathSegments) - superstr', function () { @@ -618,31 +618,31 @@ suite('Map', () => { let iter = map.findSuperstr('/user'); item = iter!.next(); - assert.equal(item.value[1], 2); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 2); + assert.strictEqual(item.done, false); item = iter!.next(); - assert.equal(item.value[1], 1); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 1); + assert.strictEqual(item.done, false); item = iter!.next(); - assert.equal(item.value[1], 3); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 3); + assert.strictEqual(item.done, false); item = iter!.next(); - assert.equal(item.value, undefined); - assert.equal(item.done, true); + assert.strictEqual(item.value, undefined); + assert.strictEqual(item.done, true); iter = map.findSuperstr('/usr'); item = iter!.next(); - assert.equal(item.value[1], 4); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 4); + assert.strictEqual(item.done, false); item = iter!.next(); - assert.equal(item.value, undefined); - assert.equal(item.done, true); + assert.strictEqual(item.value, undefined); + assert.strictEqual(item.done, true); - assert.equal(map.findSuperstr('/not'), undefined); - assert.equal(map.findSuperstr('/us'), undefined); - assert.equal(map.findSuperstr('/usrr'), undefined); - assert.equal(map.findSuperstr('/userr'), undefined); + assert.strictEqual(map.findSuperstr('/not'), undefined); + assert.strictEqual(map.findSuperstr('/us'), undefined); + assert.strictEqual(map.findSuperstr('/usrr'), undefined); + assert.strictEqual(map.findSuperstr('/userr'), undefined); }); @@ -688,16 +688,16 @@ suite('Map', () => { trie.set(URI.file('/user/foo'), 2); trie.set(URI.file('/user/foo/flip/flop'), 3); - assert.equal(trie.get(URI.file('/user/foo/bar')), 1); - assert.equal(trie.get(URI.file('/user/foo')), 2); - assert.equal(trie.get(URI.file('/user/foo/flip/flop')), 3); + assert.strictEqual(trie.get(URI.file('/user/foo/bar')), 1); + assert.strictEqual(trie.get(URI.file('/user/foo')), 2); + assert.strictEqual(trie.get(URI.file('/user/foo/flip/flop')), 3); - assert.equal(trie.findSubstr(URI.file('/user/bar')), undefined); - assert.equal(trie.findSubstr(URI.file('/user/foo')), 2); - assert.equal(trie.findSubstr(URI.file('/user/foo/ba')), 2); - assert.equal(trie.findSubstr(URI.file('/user/foo/far/boo')), 2); - assert.equal(trie.findSubstr(URI.file('/user/foo/bar')), 1); - assert.equal(trie.findSubstr(URI.file('/user/foo/bar/far/boo')), 1); + assert.strictEqual(trie.findSubstr(URI.file('/user/bar')), undefined); + assert.strictEqual(trie.findSubstr(URI.file('/user/foo')), 2); + assert.strictEqual(trie.findSubstr(URI.file('/user/foo/ba')), 2); + assert.strictEqual(trie.findSubstr(URI.file('/user/foo/far/boo')), 2); + assert.strictEqual(trie.findSubstr(URI.file('/user/foo/bar')), 1); + assert.strictEqual(trie.findSubstr(URI.file('/user/foo/bar/far/boo')), 1); }); test('TernarySearchTree (URI) - lookup', function () { @@ -708,23 +708,23 @@ suite('Map', () => { map.set(URI.parse('http://foo.bar/user/foo?QUERY'), 3); map.set(URI.parse('http://foo.bar/user/foo/flip/flop'), 3); - assert.equal(map.get(URI.parse('http://foo.bar/foo')), undefined); - assert.equal(map.get(URI.parse('http://foo.bar/user')), undefined); - assert.equal(map.get(URI.parse('http://foo.bar/user/foo/bar')), 1); - assert.equal(map.get(URI.parse('http://foo.bar/user/foo?query')), 2); - assert.equal(map.get(URI.parse('http://foo.bar/user/foo?Query')), undefined); - assert.equal(map.get(URI.parse('http://foo.bar/user/foo?QUERY')), 3); - assert.equal(map.get(URI.parse('http://foo.bar/user/foo/bar/boo')), undefined); + assert.strictEqual(map.get(URI.parse('http://foo.bar/foo')), undefined); + assert.strictEqual(map.get(URI.parse('http://foo.bar/user')), undefined); + assert.strictEqual(map.get(URI.parse('http://foo.bar/user/foo/bar')), 1); + assert.strictEqual(map.get(URI.parse('http://foo.bar/user/foo?query')), 2); + assert.strictEqual(map.get(URI.parse('http://foo.bar/user/foo?Query')), undefined); + assert.strictEqual(map.get(URI.parse('http://foo.bar/user/foo?QUERY')), 3); + assert.strictEqual(map.get(URI.parse('http://foo.bar/user/foo/bar/boo')), undefined); }); test('TernarySearchTree (URI) - lookup, casing', function () { const map = new TernarySearchTree(new UriIterator(uri => /^https?$/.test(uri.scheme))); map.set(URI.parse('http://foo.bar/user/foo/bar'), 1); - assert.equal(map.get(URI.parse('http://foo.bar/USER/foo/bar')), 1); + assert.strictEqual(map.get(URI.parse('http://foo.bar/USER/foo/bar')), 1); map.set(URI.parse('foo://foo.bar/user/foo/bar'), 1); - assert.equal(map.get(URI.parse('foo://foo.bar/USER/foo/bar')), undefined); + assert.strictEqual(map.get(URI.parse('foo://foo.bar/USER/foo/bar')), undefined); }); test('TernarySearchTree (URI) - superstr', function () { @@ -739,48 +739,48 @@ suite('Map', () => { let iter = map.findSuperstr(URI.file('/user'))!; item = iter.next(); - assert.equal(item.value[1], 2); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 2); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value[1], 1); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 1); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value[1], 3); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 3); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value, undefined); - assert.equal(item.done, true); + assert.strictEqual(item.value, undefined); + assert.strictEqual(item.done, true); iter = map.findSuperstr(URI.file('/usr'))!; item = iter.next(); - assert.equal(item.value[1], 4); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 4); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value, undefined); - assert.equal(item.done, true); + assert.strictEqual(item.value, undefined); + assert.strictEqual(item.done, true); iter = map.findSuperstr(URI.file('/'))!; item = iter.next(); - assert.equal(item.value[1], 2); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 2); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value[1], 1); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 1); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value[1], 3); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 3); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value[1], 4); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 4); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value, undefined); - assert.equal(item.done, true); + assert.strictEqual(item.value, undefined); + assert.strictEqual(item.done, true); - assert.equal(map.findSuperstr(URI.file('/not')), undefined); - assert.equal(map.findSuperstr(URI.file('/us')), undefined); - assert.equal(map.findSuperstr(URI.file('/usrr')), undefined); - assert.equal(map.findSuperstr(URI.file('/userr')), undefined); + assert.strictEqual(map.findSuperstr(URI.file('/not')), undefined); + assert.strictEqual(map.findSuperstr(URI.file('/us')), undefined); + assert.strictEqual(map.findSuperstr(URI.file('/usrr')), undefined); + assert.strictEqual(map.findSuperstr(URI.file('/userr')), undefined); }); test('TernarySearchTree (ConfigKeySegments) - basics', function () { @@ -790,16 +790,16 @@ suite('Map', () => { trie.set('config.foo', 2); trie.set('config.foo.flip.flop', 3); - assert.equal(trie.get('config.foo.bar'), 1); - assert.equal(trie.get('config.foo'), 2); - assert.equal(trie.get('config.foo.flip.flop'), 3); + assert.strictEqual(trie.get('config.foo.bar'), 1); + assert.strictEqual(trie.get('config.foo'), 2); + assert.strictEqual(trie.get('config.foo.flip.flop'), 3); - assert.equal(trie.findSubstr('config.bar'), undefined); - assert.equal(trie.findSubstr('config.foo'), 2); - assert.equal(trie.findSubstr('config.foo.ba'), 2); - assert.equal(trie.findSubstr('config.foo.far.boo'), 2); - assert.equal(trie.findSubstr('config.foo.bar'), 1); - assert.equal(trie.findSubstr('config.foo.bar.far.boo'), 1); + assert.strictEqual(trie.findSubstr('config.bar'), undefined); + assert.strictEqual(trie.findSubstr('config.foo'), 2); + assert.strictEqual(trie.findSubstr('config.foo.ba'), 2); + assert.strictEqual(trie.findSubstr('config.foo.far.boo'), 2); + assert.strictEqual(trie.findSubstr('config.foo.bar'), 1); + assert.strictEqual(trie.findSubstr('config.foo.bar.far.boo'), 1); }); test('TernarySearchTree (ConfigKeySegments) - lookup', function () { @@ -809,11 +809,11 @@ suite('Map', () => { map.set('config.foo', 2); map.set('config.foo.flip.flop', 3); - assert.equal(map.get('foo'), undefined); - assert.equal(map.get('config'), undefined); - assert.equal(map.get('config.foo'), 2); - assert.equal(map.get('config.foo.bar'), 1); - assert.equal(map.get('config.foo.bar.boo'), undefined); + assert.strictEqual(map.get('foo'), undefined); + assert.strictEqual(map.get('config'), undefined); + assert.strictEqual(map.get('config.foo'), 2); + assert.strictEqual(map.get('config.foo.bar'), 1); + assert.strictEqual(map.get('config.foo.bar.boo'), undefined); }); test('TernarySearchTree (ConfigKeySegments) - superstr', function () { @@ -828,21 +828,21 @@ suite('Map', () => { let iter = map.findSuperstr('config'); item = iter!.next(); - assert.equal(item.value[1], 2); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 2); + assert.strictEqual(item.done, false); item = iter!.next(); - assert.equal(item.value[1], 1); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 1); + assert.strictEqual(item.done, false); item = iter!.next(); - assert.equal(item.value[1], 3); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 3); + assert.strictEqual(item.done, false); item = iter!.next(); - assert.equal(item.value, undefined); - assert.equal(item.done, true); + assert.strictEqual(item.value, undefined); + assert.strictEqual(item.done, true); - assert.equal(map.findSuperstr('foo'), undefined); - assert.equal(map.findSuperstr('config.foo.no'), undefined); - assert.equal(map.findSuperstr('config.foop'), undefined); + assert.strictEqual(map.findSuperstr('foo'), undefined); + assert.strictEqual(map.findSuperstr('config.foo.no'), undefined); + assert.strictEqual(map.findSuperstr('config.foop'), undefined); }); @@ -891,7 +891,7 @@ suite('Map', () => { const resource5 = URI.parse('some://5'); const resource6 = URI.parse('some://6'); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); let res = map.set(resource1, 1); assert.ok(res === map); @@ -899,13 +899,13 @@ suite('Map', () => { map.set(resource3, true); const values = [...map.values()]; - assert.equal(values[0], 1); - assert.equal(values[1], '2'); - assert.equal(values[2], true); + assert.strictEqual(values[0], 1); + assert.strictEqual(values[1], '2'); + assert.strictEqual(values[2], true); let counter = 0; map.forEach((value, key, mapObj) => { - assert.equal(value, values[counter++]); + assert.strictEqual(value, values[counter++]); assert.ok(URI.isUri(key)); assert.ok(map === mapObj); }); @@ -916,23 +916,23 @@ suite('Map', () => { const date = Date.now(); map.set(resource5, date); - assert.equal(map.size, 5); - assert.equal(map.get(resource1), 1); - assert.equal(map.get(resource2), '2'); - assert.equal(map.get(resource3), true); - assert.equal(map.get(resource4), obj); - assert.equal(map.get(resource5), date); + assert.strictEqual(map.size, 5); + assert.strictEqual(map.get(resource1), 1); + assert.strictEqual(map.get(resource2), '2'); + assert.strictEqual(map.get(resource3), true); + assert.strictEqual(map.get(resource4), obj); + assert.strictEqual(map.get(resource5), date); assert.ok(!map.get(resource6)); map.delete(resource6); - assert.equal(map.size, 5); + assert.strictEqual(map.size, 5); assert.ok(map.delete(resource1)); assert.ok(map.delete(resource2)); assert.ok(map.delete(resource3)); assert.ok(map.delete(resource4)); assert.ok(map.delete(resource5)); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); assert.ok(!map.get(resource5)); assert.ok(!map.get(resource4)); assert.ok(!map.get(resource3)); @@ -944,13 +944,13 @@ suite('Map', () => { map.set(resource3, true); assert.ok(map.has(resource1)); - assert.equal(map.get(resource1), 1); - assert.equal(map.get(resource2), '2'); - assert.equal(map.get(resource3), true); + assert.strictEqual(map.get(resource1), 1); + assert.strictEqual(map.get(resource2), '2'); + assert.strictEqual(map.get(resource3), true); map.clear(); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); assert.ok(!map.get(resource1)); assert.ok(!map.get(resource2)); assert.ok(!map.get(resource3)); @@ -971,16 +971,16 @@ suite('Map', () => { const fileAUpper = URI.parse('file://SOME/FILEA'); map.set(fileA, 'true'); - assert.equal(map.get(fileA), 'true'); + assert.strictEqual(map.get(fileA), 'true'); assert.ok(!map.get(fileAUpper)); assert.ok(!map.get(fileB)); map.set(fileAUpper, 'false'); - assert.equal(map.get(fileAUpper), 'false'); + assert.strictEqual(map.get(fileAUpper), 'false'); - assert.equal(map.get(fileA), 'true'); + assert.strictEqual(map.get(fileA), 'true'); const windowsFile = URI.file('c:\\test with %25\\c#code'); const uncFile = URI.file('\\\\shäres\\path\\c#\\plugin.json'); @@ -988,8 +988,8 @@ suite('Map', () => { map.set(windowsFile, 'true'); map.set(uncFile, 'true'); - assert.equal(map.get(windowsFile), 'true'); - assert.equal(map.get(uncFile), 'true'); + assert.strictEqual(map.get(windowsFile), 'true'); + assert.strictEqual(map.get(uncFile), 'true'); }); test('ResourceMap - files (ignorecase)', function () { @@ -1000,16 +1000,16 @@ suite('Map', () => { const fileAUpper = URI.parse('file://SOME/FILEA'); map.set(fileA, 'true'); - assert.equal(map.get(fileA), 'true'); + assert.strictEqual(map.get(fileA), 'true'); - assert.equal(map.get(fileAUpper), 'true'); + assert.strictEqual(map.get(fileAUpper), 'true'); assert.ok(!map.get(fileB)); map.set(fileAUpper, 'false'); - assert.equal(map.get(fileAUpper), 'false'); + assert.strictEqual(map.get(fileAUpper), 'false'); - assert.equal(map.get(fileA), 'false'); + assert.strictEqual(map.get(fileA), 'false'); const windowsFile = URI.file('c:\\test with %25\\c#code'); const uncFile = URI.file('\\\\shäres\\path\\c#\\plugin.json'); @@ -1017,7 +1017,7 @@ suite('Map', () => { map.set(windowsFile, 'true'); map.set(uncFile, 'true'); - assert.equal(map.get(windowsFile), 'true'); - assert.equal(map.get(uncFile), 'true'); + assert.strictEqual(map.get(windowsFile), 'true'); + assert.strictEqual(map.get(uncFile), 'true'); }); }); diff --git a/src/vs/base/test/common/markdownString.test.ts b/src/vs/base/test/common/markdownString.test.ts index 424c2bd4..7d9d64f2 100644 --- a/src/vs/base/test/common/markdownString.test.ts +++ b/src/vs/base/test/common/markdownString.test.ts @@ -11,13 +11,13 @@ suite('MarkdownString', () => { test('Escape leading whitespace', function () { const mds = new MarkdownString(); mds.appendText('Hello\n Not a code block'); - assert.equal(mds.value, 'Hello\n\n    Not a code block'); + assert.strictEqual(mds.value, 'Hello\n\n    Not a code block'); }); test('MarkdownString.appendText doesn\'t escape quote #109040', function () { const mds = new MarkdownString(); mds.appendText('> Text\n>More'); - assert.equal(mds.value, '\\> Text\n\n\\>More'); + assert.strictEqual(mds.value, '\\> Text\n\n\\>More'); }); test('appendText', () => { @@ -25,7 +25,7 @@ suite('MarkdownString', () => { const mds = new MarkdownString(); mds.appendText('# foo\n*bar*'); - assert.equal(mds.value, '\\# foo\n\n\\*bar\\*'); + assert.strictEqual(mds.value, '\\# foo\n\n\\*bar\\*'); }); suite('ThemeIcons', () => { @@ -36,21 +36,21 @@ suite('MarkdownString', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: true }); mds.appendText('$(zap) $(not a theme icon) $(add)'); - assert.equal(mds.value, '\\\\$\\(zap\\) $\\(not a theme icon\\) \\\\$\\(add\\)'); + assert.strictEqual(mds.value, '\\\\$\\(zap\\) $\\(not a theme icon\\) \\\\$\\(add\\)'); }); test('appendMarkdown', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: true }); mds.appendMarkdown('$(zap) $(not a theme icon) $(add)'); - assert.equal(mds.value, '$(zap) $(not a theme icon) $(add)'); + assert.strictEqual(mds.value, '$(zap) $(not a theme icon) $(add)'); }); test('appendMarkdown with escaped icon', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: true }); mds.appendMarkdown('\\$(zap) $(not a theme icon) $(add)'); - assert.equal(mds.value, '\\$(zap) $(not a theme icon) $(add)'); + assert.strictEqual(mds.value, '\\$(zap) $(not a theme icon) $(add)'); }); }); @@ -61,21 +61,21 @@ suite('MarkdownString', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: false }); mds.appendText('$(zap) $(not a theme icon) $(add)'); - assert.equal(mds.value, '$\\(zap\\) $\\(not a theme icon\\) $\\(add\\)'); + assert.strictEqual(mds.value, '$\\(zap\\) $\\(not a theme icon\\) $\\(add\\)'); }); test('appendMarkdown', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: false }); mds.appendMarkdown('$(zap) $(not a theme icon) $(add)'); - assert.equal(mds.value, '$(zap) $(not a theme icon) $(add)'); + assert.strictEqual(mds.value, '$(zap) $(not a theme icon) $(add)'); }); test('appendMarkdown with escaped icon', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: true }); mds.appendMarkdown('\\$(zap) $(not a theme icon) $(add)'); - assert.equal(mds.value, '\\$(zap) $(not a theme icon) $(add)'); + assert.strictEqual(mds.value, '\\$(zap) $(not a theme icon) $(add)'); }); }); diff --git a/src/vs/base/test/common/mime.test.ts b/src/vs/base/test/common/mime.test.ts index a7ac5a17..f2583fed 100644 --- a/src/vs/base/test/common/mime.test.ts +++ b/src/vs/base/test/common/mime.test.ts @@ -11,38 +11,38 @@ suite('Mime', () => { test('Dynamically Register Text Mime', () => { let guess = guessMimeTypes(URI.file('foo.monaco')); - assert.deepEqual(guess, ['application/unknown']); + assert.deepStrictEqual(guess, ['application/unknown']); registerTextMime({ id: 'monaco', extension: '.monaco', mime: 'text/monaco' }); guess = guessMimeTypes(URI.file('foo.monaco')); - assert.deepEqual(guess, ['text/monaco', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco', 'text/plain']); guess = guessMimeTypes(URI.file('.monaco')); - assert.deepEqual(guess, ['text/monaco', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco', 'text/plain']); registerTextMime({ id: 'codefile', filename: 'Codefile', mime: 'text/code' }); guess = guessMimeTypes(URI.file('Codefile')); - assert.deepEqual(guess, ['text/code', 'text/plain']); + assert.deepStrictEqual(guess, ['text/code', 'text/plain']); guess = guessMimeTypes(URI.file('foo.Codefile')); - assert.deepEqual(guess, ['application/unknown']); + assert.deepStrictEqual(guess, ['application/unknown']); registerTextMime({ id: 'docker', filepattern: 'Docker*', mime: 'text/docker' }); guess = guessMimeTypes(URI.file('Docker-debug')); - assert.deepEqual(guess, ['text/docker', 'text/plain']); + assert.deepStrictEqual(guess, ['text/docker', 'text/plain']); guess = guessMimeTypes(URI.file('docker-PROD')); - assert.deepEqual(guess, ['text/docker', 'text/plain']); + assert.deepStrictEqual(guess, ['text/docker', 'text/plain']); registerTextMime({ id: 'niceregex', mime: 'text/nice-regex', firstline: /RegexesAreNice/ }); guess = guessMimeTypes(URI.file('Randomfile.noregistration'), 'RegexesAreNice'); - assert.deepEqual(guess, ['text/nice-regex', 'text/plain']); + assert.deepStrictEqual(guess, ['text/nice-regex', 'text/plain']); guess = guessMimeTypes(URI.file('Randomfile.noregistration'), 'RegexesAreNotNice'); - assert.deepEqual(guess, ['application/unknown']); + assert.deepStrictEqual(guess, ['application/unknown']); guess = guessMimeTypes(URI.file('Codefile'), 'RegexesAreNice'); - assert.deepEqual(guess, ['text/code', 'text/plain']); + assert.deepStrictEqual(guess, ['text/code', 'text/plain']); }); test('Mimes Priority', () => { @@ -50,36 +50,36 @@ suite('Mime', () => { registerTextMime({ id: 'foobar', mime: 'text/foobar', firstline: /foobar/ }); let guess = guessMimeTypes(URI.file('foo.monaco')); - assert.deepEqual(guess, ['text/monaco', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco', 'text/plain']); guess = guessMimeTypes(URI.file('foo.monaco'), 'foobar'); - assert.deepEqual(guess, ['text/monaco', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco', 'text/plain']); registerTextMime({ id: 'docker', filename: 'dockerfile', mime: 'text/winner' }); registerTextMime({ id: 'docker', filepattern: 'dockerfile*', mime: 'text/looser' }); guess = guessMimeTypes(URI.file('dockerfile')); - assert.deepEqual(guess, ['text/winner', 'text/plain']); + assert.deepStrictEqual(guess, ['text/winner', 'text/plain']); registerTextMime({ id: 'azure-looser', mime: 'text/azure-looser', firstline: /azure/ }); registerTextMime({ id: 'azure-winner', mime: 'text/azure-winner', firstline: /azure/ }); guess = guessMimeTypes(URI.file('azure'), 'azure'); - assert.deepEqual(guess, ['text/azure-winner', 'text/plain']); + assert.deepStrictEqual(guess, ['text/azure-winner', 'text/plain']); }); test('Specificity priority 1', () => { registerTextMime({ id: 'monaco2', extension: '.monaco2', mime: 'text/monaco2' }); registerTextMime({ id: 'monaco2', filename: 'specific.monaco2', mime: 'text/specific-monaco2' }); - assert.deepEqual(guessMimeTypes(URI.file('specific.monaco2')), ['text/specific-monaco2', 'text/plain']); - assert.deepEqual(guessMimeTypes(URI.file('foo.monaco2')), ['text/monaco2', 'text/plain']); + assert.deepStrictEqual(guessMimeTypes(URI.file('specific.monaco2')), ['text/specific-monaco2', 'text/plain']); + assert.deepStrictEqual(guessMimeTypes(URI.file('foo.monaco2')), ['text/monaco2', 'text/plain']); }); test('Specificity priority 2', () => { registerTextMime({ id: 'monaco3', filename: 'specific.monaco3', mime: 'text/specific-monaco3' }); registerTextMime({ id: 'monaco3', extension: '.monaco3', mime: 'text/monaco3' }); - assert.deepEqual(guessMimeTypes(URI.file('specific.monaco3')), ['text/specific-monaco3', 'text/plain']); - assert.deepEqual(guessMimeTypes(URI.file('foo.monaco3')), ['text/monaco3', 'text/plain']); + assert.deepStrictEqual(guessMimeTypes(URI.file('specific.monaco3')), ['text/specific-monaco3', 'text/plain']); + assert.deepStrictEqual(guessMimeTypes(URI.file('foo.monaco3')), ['text/monaco3', 'text/plain']); }); test('Mimes Priority - Longest Extension wins', () => { @@ -88,13 +88,13 @@ suite('Mime', () => { registerTextMime({ id: 'monaco', extension: '.monaco.xml.build', mime: 'text/monaco-xml-build' }); let guess = guessMimeTypes(URI.file('foo.monaco')); - assert.deepEqual(guess, ['text/monaco', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco', 'text/plain']); guess = guessMimeTypes(URI.file('foo.monaco.xml')); - assert.deepEqual(guess, ['text/monaco-xml', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco-xml', 'text/plain']); guess = guessMimeTypes(URI.file('foo.monaco.xml.build')); - assert.deepEqual(guess, ['text/monaco-xml-build', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco-xml-build', 'text/plain']); }); test('Mimes Priority - User configured wins', () => { @@ -102,7 +102,7 @@ suite('Mime', () => { registerTextMime({ id: 'monaco', extension: '.monaco.xml', mime: 'text/monaco-xml' }); let guess = guessMimeTypes(URI.file('foo.monaco.xnl')); - assert.deepEqual(guess, ['text/monaco', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco', 'text/plain']); }); test('Mimes Priority - Pattern matches on path if specified', () => { @@ -110,7 +110,7 @@ suite('Mime', () => { registerTextMime({ id: 'other', filepattern: '*ot.other.xml', mime: 'text/other' }); let guess = guessMimeTypes(URI.file('/some/path/dot.monaco.xml')); - assert.deepEqual(guess, ['text/monaco', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco', 'text/plain']); }); test('Mimes Priority - Last registered mime wins', () => { @@ -118,12 +118,12 @@ suite('Mime', () => { registerTextMime({ id: 'other', filepattern: '**/dot.monaco.xml', mime: 'text/other' }); let guess = guessMimeTypes(URI.file('/some/path/dot.monaco.xml')); - assert.deepEqual(guess, ['text/other', 'text/plain']); + assert.deepStrictEqual(guess, ['text/other', 'text/plain']); }); test('Data URIs', () => { registerTextMime({ id: 'data', extension: '.data', mime: 'text/data' }); - assert.deepEqual(guessMimeTypes(URI.parse(`data:;label:something.data;description:data,`)), ['text/data', 'text/plain']); + assert.deepStrictEqual(guessMimeTypes(URI.parse(`data:;label:something.data;description:data,`)), ['text/data', 'text/plain']); }); }); diff --git a/src/vs/base/test/common/network.test.ts b/src/vs/base/test/common/network.test.ts index 1729d4b1..240c1abc 100644 --- a/src/vs/base/test/common/network.test.ts +++ b/src/vs/base/test/common/network.test.ts @@ -7,10 +7,10 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { FileAccess, Schemas } from 'vs/base/common/network'; import { isEqual } from 'vs/base/common/resources'; -import { isElectronSandboxed } from 'vs/base/common/platform'; +import { isPreferringBrowserCodeLoad } from 'vs/base/common/platform'; suite('network', () => { - const enableTest = isElectronSandboxed; + const enableTest = isPreferringBrowserCodeLoad; (!enableTest ? test.skip : test)('FileAccess: URI (native)', () => { diff --git a/src/vs/base/test/common/paging.test.ts b/src/vs/base/test/common/paging.test.ts index 3ce69663..da7ef18f 100644 --- a/src/vs/base/test/common/paging.test.ts +++ b/src/vs/base/test/common/paging.test.ts @@ -101,7 +101,6 @@ suite('PagedModel', () => { test('preemptive cancellation works', async function () { const pager = new TestPager(() => { assert(false); - return Promise.resolve([]); }); const model = new PagedModel(pager); diff --git a/src/vs/base/test/node/path.test.ts b/src/vs/base/test/common/path.test.ts similarity index 99% rename from src/vs/base/test/node/path.test.ts rename to src/vs/base/test/common/path.test.ts index 3150f8d6..d74ce9f7 100644 --- a/src/vs/base/test/node/path.test.ts +++ b/src/vs/base/test/common/path.test.ts @@ -29,10 +29,11 @@ import * as assert from 'assert'; import * as path from 'vs/base/common/path'; -import { isWindows } from 'vs/base/common/platform'; +import { isWeb, isWindows } from 'vs/base/common/platform'; import * as process from 'vs/base/common/process'; suite('Paths (Node Implementation)', () => { + const __filename = 'path.test.js'; test('join', () => { const failures = [] as string[]; const backslashRE = /\\/g; @@ -175,9 +176,6 @@ suite('Paths (Node Implementation)', () => { }); test('dirname', () => { - assert.strictEqual(path.dirname(path.normalize(__filename)).substr(-9), - isWindows ? 'test\\node' : 'test/node'); - assert.strictEqual(path.posix.dirname('/a/b/'), '/a'); assert.strictEqual(path.posix.dirname('/a/b'), '/a'); assert.strictEqual(path.posix.dirname('/a'), '/'); @@ -362,7 +360,7 @@ suite('Paths (Node Implementation)', () => { assert.equal(path.extname('far.boo/boo'), ''); }); - test('resolve', () => { + (isWeb && isWindows ? test.skip : test)('resolve', () => { // TODO@sbatten fails on windows & browser only const failures = [] as string[]; const slashRE = /\//g; const backslashRE = /\\/g; diff --git a/src/vs/base/test/common/processes.test.ts b/src/vs/base/test/common/processes.test.ts index ee0d25c8..b5a89e2a 100644 --- a/src/vs/base/test/common/processes.test.ts +++ b/src/vs/base/test/common/processes.test.ts @@ -15,7 +15,6 @@ suite('Processes', () => { ELECTRON_NO_ASAR: 'x', ELECTRON_NO_ATTACH_CONSOLE: 'x', ELECTRON_RUN_AS_NODE: 'x', - GOOGLE_API_KEY: 'x', VSCODE_CLI: 'x', VSCODE_DEV: 'x', VSCODE_IPC_HOOK: 'x', diff --git a/src/vs/base/test/common/resources.test.ts b/src/vs/base/test/common/resources.test.ts index 8026e240..a41725d8 100644 --- a/src/vs/base/test/common/resources.test.ts +++ b/src/vs/base/test/common/resources.test.ts @@ -22,10 +22,10 @@ suite('Resources', () => { ]; let distinct = distinctParents(resources, r => r); - assert.equal(distinct.length, 3); - assert.equal(distinct[0].toString(), resources[0].toString()); - assert.equal(distinct[1].toString(), resources[1].toString()); - assert.equal(distinct[2].toString(), resources[2].toString()); + assert.strictEqual(distinct.length, 3); + assert.strictEqual(distinct[0].toString(), resources[0].toString()); + assert.strictEqual(distinct[1].toString(), resources[1].toString()); + assert.strictEqual(distinct[2].toString(), resources[2].toString()); // Parent / Child resources = [ @@ -37,144 +37,144 @@ suite('Resources', () => { ]; distinct = distinctParents(resources, r => r); - assert.equal(distinct.length, 3); - assert.equal(distinct[0].toString(), resources[0].toString()); - assert.equal(distinct[1].toString(), resources[3].toString()); - assert.equal(distinct[2].toString(), resources[4].toString()); + assert.strictEqual(distinct.length, 3); + assert.strictEqual(distinct[0].toString(), resources[0].toString()); + assert.strictEqual(distinct[1].toString(), resources[3].toString()); + assert.strictEqual(distinct[2].toString(), resources[4].toString()); }); test('dirname', () => { if (isWindows) { - assert.equal(dirname(URI.file('c:\\some\\file\\test.txt')).toString(), 'file:///c%3A/some/file'); - assert.equal(dirname(URI.file('c:\\some\\file')).toString(), 'file:///c%3A/some'); - assert.equal(dirname(URI.file('c:\\some\\file\\')).toString(), 'file:///c%3A/some'); - assert.equal(dirname(URI.file('c:\\some')).toString(), 'file:///c%3A/'); - assert.equal(dirname(URI.file('C:\\some')).toString(), 'file:///c%3A/'); - assert.equal(dirname(URI.file('c:\\')).toString(), 'file:///c%3A/'); + assert.strictEqual(dirname(URI.file('c:\\some\\file\\test.txt')).toString(), 'file:///c%3A/some/file'); + assert.strictEqual(dirname(URI.file('c:\\some\\file')).toString(), 'file:///c%3A/some'); + assert.strictEqual(dirname(URI.file('c:\\some\\file\\')).toString(), 'file:///c%3A/some'); + assert.strictEqual(dirname(URI.file('c:\\some')).toString(), 'file:///c%3A/'); + assert.strictEqual(dirname(URI.file('C:\\some')).toString(), 'file:///c%3A/'); + assert.strictEqual(dirname(URI.file('c:\\')).toString(), 'file:///c%3A/'); } else { - assert.equal(dirname(URI.file('/some/file/test.txt')).toString(), 'file:///some/file'); - assert.equal(dirname(URI.file('/some/file/')).toString(), 'file:///some'); - assert.equal(dirname(URI.file('/some/file')).toString(), 'file:///some'); + assert.strictEqual(dirname(URI.file('/some/file/test.txt')).toString(), 'file:///some/file'); + assert.strictEqual(dirname(URI.file('/some/file/')).toString(), 'file:///some'); + assert.strictEqual(dirname(URI.file('/some/file')).toString(), 'file:///some'); } - assert.equal(dirname(URI.parse('foo://a/some/file/test.txt')).toString(), 'foo://a/some/file'); - assert.equal(dirname(URI.parse('foo://a/some/file/')).toString(), 'foo://a/some'); - assert.equal(dirname(URI.parse('foo://a/some/file')).toString(), 'foo://a/some'); - assert.equal(dirname(URI.parse('foo://a/some')).toString(), 'foo://a/'); - assert.equal(dirname(URI.parse('foo://a/')).toString(), 'foo://a/'); - assert.equal(dirname(URI.parse('foo://a')).toString(), 'foo://a'); + assert.strictEqual(dirname(URI.parse('foo://a/some/file/test.txt')).toString(), 'foo://a/some/file'); + assert.strictEqual(dirname(URI.parse('foo://a/some/file/')).toString(), 'foo://a/some'); + assert.strictEqual(dirname(URI.parse('foo://a/some/file')).toString(), 'foo://a/some'); + assert.strictEqual(dirname(URI.parse('foo://a/some')).toString(), 'foo://a/'); + assert.strictEqual(dirname(URI.parse('foo://a/')).toString(), 'foo://a/'); + assert.strictEqual(dirname(URI.parse('foo://a')).toString(), 'foo://a'); // does not explode (https://github.com/microsoft/vscode/issues/41987) dirname(URI.from({ scheme: 'file', authority: '/users/someone/portal.h' })); - assert.equal(dirname(URI.parse('foo://a/b/c?q')).toString(), 'foo://a/b?q'); + assert.strictEqual(dirname(URI.parse('foo://a/b/c?q')).toString(), 'foo://a/b?q'); }); test('basename', () => { if (isWindows) { - assert.equal(basename(URI.file('c:\\some\\file\\test.txt')), 'test.txt'); - assert.equal(basename(URI.file('c:\\some\\file')), 'file'); - assert.equal(basename(URI.file('c:\\some\\file\\')), 'file'); - assert.equal(basename(URI.file('C:\\some\\file\\')), 'file'); + assert.strictEqual(basename(URI.file('c:\\some\\file\\test.txt')), 'test.txt'); + assert.strictEqual(basename(URI.file('c:\\some\\file')), 'file'); + assert.strictEqual(basename(URI.file('c:\\some\\file\\')), 'file'); + assert.strictEqual(basename(URI.file('C:\\some\\file\\')), 'file'); } else { - assert.equal(basename(URI.file('/some/file/test.txt')), 'test.txt'); - assert.equal(basename(URI.file('/some/file/')), 'file'); - assert.equal(basename(URI.file('/some/file')), 'file'); - assert.equal(basename(URI.file('/some')), 'some'); + assert.strictEqual(basename(URI.file('/some/file/test.txt')), 'test.txt'); + assert.strictEqual(basename(URI.file('/some/file/')), 'file'); + assert.strictEqual(basename(URI.file('/some/file')), 'file'); + assert.strictEqual(basename(URI.file('/some')), 'some'); } - assert.equal(basename(URI.parse('foo://a/some/file/test.txt')), 'test.txt'); - assert.equal(basename(URI.parse('foo://a/some/file/')), 'file'); - assert.equal(basename(URI.parse('foo://a/some/file')), 'file'); - assert.equal(basename(URI.parse('foo://a/some')), 'some'); - assert.equal(basename(URI.parse('foo://a/')), ''); - assert.equal(basename(URI.parse('foo://a')), ''); + assert.strictEqual(basename(URI.parse('foo://a/some/file/test.txt')), 'test.txt'); + assert.strictEqual(basename(URI.parse('foo://a/some/file/')), 'file'); + assert.strictEqual(basename(URI.parse('foo://a/some/file')), 'file'); + assert.strictEqual(basename(URI.parse('foo://a/some')), 'some'); + assert.strictEqual(basename(URI.parse('foo://a/')), ''); + assert.strictEqual(basename(URI.parse('foo://a')), ''); }); test('joinPath', () => { if (isWindows) { - assert.equal(joinPath(URI.file('c:\\foo\\bar'), '/file.js').toString(), 'file:///c%3A/foo/bar/file.js'); - assert.equal(joinPath(URI.file('c:\\foo\\bar\\'), 'file.js').toString(), 'file:///c%3A/foo/bar/file.js'); - assert.equal(joinPath(URI.file('c:\\foo\\bar\\'), '/file.js').toString(), 'file:///c%3A/foo/bar/file.js'); - assert.equal(joinPath(URI.file('c:\\'), '/file.js').toString(), 'file:///c%3A/file.js'); - assert.equal(joinPath(URI.file('c:\\'), 'bar/file.js').toString(), 'file:///c%3A/bar/file.js'); - assert.equal(joinPath(URI.file('c:\\foo'), './file.js').toString(), 'file:///c%3A/foo/file.js'); - assert.equal(joinPath(URI.file('c:\\foo'), '/./file.js').toString(), 'file:///c%3A/foo/file.js'); - assert.equal(joinPath(URI.file('C:\\foo'), '../file.js').toString(), 'file:///c%3A/file.js'); - assert.equal(joinPath(URI.file('C:\\foo\\.'), '../file.js').toString(), 'file:///c%3A/file.js'); + assert.strictEqual(joinPath(URI.file('c:\\foo\\bar'), '/file.js').toString(), 'file:///c%3A/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('c:\\foo\\bar\\'), 'file.js').toString(), 'file:///c%3A/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('c:\\foo\\bar\\'), '/file.js').toString(), 'file:///c%3A/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('c:\\'), '/file.js').toString(), 'file:///c%3A/file.js'); + assert.strictEqual(joinPath(URI.file('c:\\'), 'bar/file.js').toString(), 'file:///c%3A/bar/file.js'); + assert.strictEqual(joinPath(URI.file('c:\\foo'), './file.js').toString(), 'file:///c%3A/foo/file.js'); + assert.strictEqual(joinPath(URI.file('c:\\foo'), '/./file.js').toString(), 'file:///c%3A/foo/file.js'); + assert.strictEqual(joinPath(URI.file('C:\\foo'), '../file.js').toString(), 'file:///c%3A/file.js'); + assert.strictEqual(joinPath(URI.file('C:\\foo\\.'), '../file.js').toString(), 'file:///c%3A/file.js'); } else { - assert.equal(joinPath(URI.file('/foo/bar'), '/file.js').toString(), 'file:///foo/bar/file.js'); - assert.equal(joinPath(URI.file('/foo/bar'), 'file.js').toString(), 'file:///foo/bar/file.js'); - assert.equal(joinPath(URI.file('/foo/bar/'), '/file.js').toString(), 'file:///foo/bar/file.js'); - assert.equal(joinPath(URI.file('/'), '/file.js').toString(), 'file:///file.js'); - assert.equal(joinPath(URI.file('/foo/bar'), './file.js').toString(), 'file:///foo/bar/file.js'); - assert.equal(joinPath(URI.file('/foo/bar'), '/./file.js').toString(), 'file:///foo/bar/file.js'); - assert.equal(joinPath(URI.file('/foo/bar'), '../file.js').toString(), 'file:///foo/file.js'); + assert.strictEqual(joinPath(URI.file('/foo/bar'), '/file.js').toString(), 'file:///foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('/foo/bar'), 'file.js').toString(), 'file:///foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('/foo/bar/'), '/file.js').toString(), 'file:///foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('/'), '/file.js').toString(), 'file:///file.js'); + assert.strictEqual(joinPath(URI.file('/foo/bar'), './file.js').toString(), 'file:///foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('/foo/bar'), '/./file.js').toString(), 'file:///foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('/foo/bar'), '../file.js').toString(), 'file:///foo/file.js'); } - assert.equal(joinPath(URI.parse('foo://a/foo/bar')).toString(), 'foo://a/foo/bar'); - assert.equal(joinPath(URI.parse('foo://a/foo/bar'), '/file.js').toString(), 'foo://a/foo/bar/file.js'); - assert.equal(joinPath(URI.parse('foo://a/foo/bar'), 'file.js').toString(), 'foo://a/foo/bar/file.js'); - assert.equal(joinPath(URI.parse('foo://a/foo/bar/'), '/file.js').toString(), 'foo://a/foo/bar/file.js'); - assert.equal(joinPath(URI.parse('foo://a/'), '/file.js').toString(), 'foo://a/file.js'); - assert.equal(joinPath(URI.parse('foo://a/foo/bar/'), './file.js').toString(), 'foo://a/foo/bar/file.js'); - assert.equal(joinPath(URI.parse('foo://a/foo/bar/'), '/./file.js').toString(), 'foo://a/foo/bar/file.js'); - assert.equal(joinPath(URI.parse('foo://a/foo/bar/'), '../file.js').toString(), 'foo://a/foo/file.js'); + assert.strictEqual(joinPath(URI.parse('foo://a/foo/bar')).toString(), 'foo://a/foo/bar'); + assert.strictEqual(joinPath(URI.parse('foo://a/foo/bar'), '/file.js').toString(), 'foo://a/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.parse('foo://a/foo/bar'), 'file.js').toString(), 'foo://a/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.parse('foo://a/foo/bar/'), '/file.js').toString(), 'foo://a/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.parse('foo://a/'), '/file.js').toString(), 'foo://a/file.js'); + assert.strictEqual(joinPath(URI.parse('foo://a/foo/bar/'), './file.js').toString(), 'foo://a/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.parse('foo://a/foo/bar/'), '/./file.js').toString(), 'foo://a/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.parse('foo://a/foo/bar/'), '../file.js').toString(), 'foo://a/foo/file.js'); - assert.equal( + assert.strictEqual( joinPath(URI.from({ scheme: 'myScheme', authority: 'authority', path: '/path', query: 'query', fragment: 'fragment' }), '/file.js').toString(), 'myScheme://authority/path/file.js?query#fragment'); }); test('normalizePath', () => { if (isWindows) { - assert.equal(normalizePath(URI.file('c:\\foo\\.\\bar')).toString(), 'file:///c%3A/foo/bar'); - assert.equal(normalizePath(URI.file('c:\\foo\\.')).toString(), 'file:///c%3A/foo'); - assert.equal(normalizePath(URI.file('c:\\foo\\.\\')).toString(), 'file:///c%3A/foo/'); - assert.equal(normalizePath(URI.file('c:\\foo\\..')).toString(), 'file:///c%3A/'); - assert.equal(normalizePath(URI.file('c:\\foo\\..\\bar')).toString(), 'file:///c%3A/bar'); - assert.equal(normalizePath(URI.file('c:\\foo\\..\\..\\bar')).toString(), 'file:///c%3A/bar'); - assert.equal(normalizePath(URI.file('c:\\foo\\foo\\..\\..\\bar')).toString(), 'file:///c%3A/bar'); - assert.equal(normalizePath(URI.file('C:\\foo\\foo\\.\\..\\..\\bar')).toString(), 'file:///c%3A/bar'); - assert.equal(normalizePath(URI.file('C:\\foo\\foo\\.\\..\\some\\..\\bar')).toString(), 'file:///c%3A/foo/bar'); + assert.strictEqual(normalizePath(URI.file('c:\\foo\\.\\bar')).toString(), 'file:///c%3A/foo/bar'); + assert.strictEqual(normalizePath(URI.file('c:\\foo\\.')).toString(), 'file:///c%3A/foo'); + assert.strictEqual(normalizePath(URI.file('c:\\foo\\.\\')).toString(), 'file:///c%3A/foo/'); + assert.strictEqual(normalizePath(URI.file('c:\\foo\\..')).toString(), 'file:///c%3A/'); + assert.strictEqual(normalizePath(URI.file('c:\\foo\\..\\bar')).toString(), 'file:///c%3A/bar'); + assert.strictEqual(normalizePath(URI.file('c:\\foo\\..\\..\\bar')).toString(), 'file:///c%3A/bar'); + assert.strictEqual(normalizePath(URI.file('c:\\foo\\foo\\..\\..\\bar')).toString(), 'file:///c%3A/bar'); + assert.strictEqual(normalizePath(URI.file('C:\\foo\\foo\\.\\..\\..\\bar')).toString(), 'file:///c%3A/bar'); + assert.strictEqual(normalizePath(URI.file('C:\\foo\\foo\\.\\..\\some\\..\\bar')).toString(), 'file:///c%3A/foo/bar'); } else { - assert.equal(normalizePath(URI.file('/foo/./bar')).toString(), 'file:///foo/bar'); - assert.equal(normalizePath(URI.file('/foo/.')).toString(), 'file:///foo'); - assert.equal(normalizePath(URI.file('/foo/./')).toString(), 'file:///foo/'); - assert.equal(normalizePath(URI.file('/foo/..')).toString(), 'file:///'); - assert.equal(normalizePath(URI.file('/foo/../bar')).toString(), 'file:///bar'); - assert.equal(normalizePath(URI.file('/foo/../../bar')).toString(), 'file:///bar'); - assert.equal(normalizePath(URI.file('/foo/foo/../../bar')).toString(), 'file:///bar'); - assert.equal(normalizePath(URI.file('/foo/foo/./../../bar')).toString(), 'file:///bar'); - assert.equal(normalizePath(URI.file('/foo/foo/./../some/../bar')).toString(), 'file:///foo/bar'); - assert.equal(normalizePath(URI.file('/f')).toString(), 'file:///f'); + assert.strictEqual(normalizePath(URI.file('/foo/./bar')).toString(), 'file:///foo/bar'); + assert.strictEqual(normalizePath(URI.file('/foo/.')).toString(), 'file:///foo'); + assert.strictEqual(normalizePath(URI.file('/foo/./')).toString(), 'file:///foo/'); + assert.strictEqual(normalizePath(URI.file('/foo/..')).toString(), 'file:///'); + assert.strictEqual(normalizePath(URI.file('/foo/../bar')).toString(), 'file:///bar'); + assert.strictEqual(normalizePath(URI.file('/foo/../../bar')).toString(), 'file:///bar'); + assert.strictEqual(normalizePath(URI.file('/foo/foo/../../bar')).toString(), 'file:///bar'); + assert.strictEqual(normalizePath(URI.file('/foo/foo/./../../bar')).toString(), 'file:///bar'); + assert.strictEqual(normalizePath(URI.file('/foo/foo/./../some/../bar')).toString(), 'file:///foo/bar'); + assert.strictEqual(normalizePath(URI.file('/f')).toString(), 'file:///f'); } - assert.equal(normalizePath(URI.parse('foo://a/foo/./bar')).toString(), 'foo://a/foo/bar'); - assert.equal(normalizePath(URI.parse('foo://a/foo/.')).toString(), 'foo://a/foo'); - assert.equal(normalizePath(URI.parse('foo://a/foo/./')).toString(), 'foo://a/foo/'); - assert.equal(normalizePath(URI.parse('foo://a/foo/..')).toString(), 'foo://a/'); - assert.equal(normalizePath(URI.parse('foo://a/foo/../bar')).toString(), 'foo://a/bar'); - assert.equal(normalizePath(URI.parse('foo://a/foo/../../bar')).toString(), 'foo://a/bar'); - assert.equal(normalizePath(URI.parse('foo://a/foo/foo/../../bar')).toString(), 'foo://a/bar'); - assert.equal(normalizePath(URI.parse('foo://a/foo/foo/./../../bar')).toString(), 'foo://a/bar'); - assert.equal(normalizePath(URI.parse('foo://a/foo/foo/./../some/../bar')).toString(), 'foo://a/foo/bar'); - assert.equal(normalizePath(URI.parse('foo://a')).toString(), 'foo://a'); - assert.equal(normalizePath(URI.parse('foo://a/')).toString(), 'foo://a/'); - assert.equal(normalizePath(URI.parse('foo://a/foo/./bar?q=1')).toString(), URI.parse('foo://a/foo/bar?q%3D1').toString()); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/./bar')).toString(), 'foo://a/foo/bar'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/.')).toString(), 'foo://a/foo'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/./')).toString(), 'foo://a/foo/'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/..')).toString(), 'foo://a/'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/../bar')).toString(), 'foo://a/bar'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/../../bar')).toString(), 'foo://a/bar'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/foo/../../bar')).toString(), 'foo://a/bar'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/foo/./../../bar')).toString(), 'foo://a/bar'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/foo/./../some/../bar')).toString(), 'foo://a/foo/bar'); + assert.strictEqual(normalizePath(URI.parse('foo://a')).toString(), 'foo://a'); + assert.strictEqual(normalizePath(URI.parse('foo://a/')).toString(), 'foo://a/'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/./bar?q=1')).toString(), URI.parse('foo://a/foo/bar?q%3D1').toString()); }); test('isAbsolute', () => { if (isWindows) { - assert.equal(isAbsolutePath(URI.file('c:\\foo\\')), true); - assert.equal(isAbsolutePath(URI.file('C:\\foo\\')), true); - assert.equal(isAbsolutePath(URI.file('bar')), true); // URI normalizes all file URIs to be absolute + assert.strictEqual(isAbsolutePath(URI.file('c:\\foo\\')), true); + assert.strictEqual(isAbsolutePath(URI.file('C:\\foo\\')), true); + assert.strictEqual(isAbsolutePath(URI.file('bar')), true); // URI normalizes all file URIs to be absolute } else { - assert.equal(isAbsolutePath(URI.file('/foo/bar')), true); - assert.equal(isAbsolutePath(URI.file('bar')), true); // URI normalizes all file URIs to be absolute + assert.strictEqual(isAbsolutePath(URI.file('/foo/bar')), true); + assert.strictEqual(isAbsolutePath(URI.file('bar')), true); // URI normalizes all file URIs to be absolute } - assert.equal(isAbsolutePath(URI.parse('foo:foo')), false); - assert.equal(isAbsolutePath(URI.parse('foo://a/foo/.')), true); + assert.strictEqual(isAbsolutePath(URI.parse('foo:foo')), false); + assert.strictEqual(isAbsolutePath(URI.parse('foo://a/foo/.')), true); }); function assertTrailingSeparator(u1: URI, expected: boolean) { - assert.equal(hasTrailingPathSeparator(u1), expected, u1.toString()); + assert.strictEqual(hasTrailingPathSeparator(u1), expected, u1.toString()); } function assertRemoveTrailingSeparator(u1: URI, expected: URI) { @@ -237,14 +237,14 @@ suite('Resources', () => { function assertEqualURI(actual: URI, expected: URI, message?: string, ignoreCase?: boolean) { let util = ignoreCase ? extUriIgnorePathCase : extUri; if (!util.isEqual(expected, actual)) { - assert.equal(actual.toString(), expected.toString(), message); + assert.strictEqual(actual.toString(), expected.toString(), message); } } function assertRelativePath(u1: URI, u2: URI, expectedPath: string | undefined, ignoreJoin?: boolean, ignoreCase?: boolean) { let util = ignoreCase ? extUriIgnorePathCase : extUri; - assert.equal(util.relativePath(u1, u2), expectedPath, `from ${u1.toString()} to ${u2.toString()}`); + assert.strictEqual(util.relativePath(u1, u2), expectedPath, `from ${u1.toString()} to ${u2.toString()}`); if (expectedPath !== undefined && !ignoreJoin) { assertEqualURI(removeTrailingPathSeparator(joinPath(u1, expectedPath)), removeTrailingPathSeparator(u2), 'joinPath on relativePath should be equal', ignoreCase); } @@ -304,7 +304,7 @@ suite('Resources', () => { if (!p.isAbsolute(path)) { let expectedPath = isWindows ? toSlashes(path) : path; expectedPath = expectedPath.startsWith('./') ? expectedPath.substr(2) : expectedPath; - assert.equal(relativePath(u1, actual), expectedPath, `relativePath (${u1.toString()}) on actual (${actual.toString()}) should be to path (${expectedPath})`); + assert.strictEqual(relativePath(u1, actual), expectedPath, `relativePath (${u1.toString()}) on actual (${actual.toString()}) should be to path (${expectedPath})`); } } @@ -352,12 +352,12 @@ suite('Resources', () => { let util = ignoreCase ? extUriIgnorePathCase : extUri; - assert.equal(util.isEqual(u1, u2), expected, `${u1.toString()}${expected ? '===' : '!=='}${u2.toString()}`); - assert.equal(util.compare(u1, u2) === 0, expected); - assert.equal(util.getComparisonKey(u1) === util.getComparisonKey(u2), expected, `comparison keys ${u1.toString()}, ${u2.toString()}`); - assert.equal(util.isEqualOrParent(u1, u2), expected, `isEqualOrParent ${u1.toString()}, ${u2.toString()}`); + assert.strictEqual(util.isEqual(u1, u2), expected, `${u1.toString()}${expected ? '===' : '!=='}${u2.toString()}`); + assert.strictEqual(util.compare(u1, u2) === 0, expected); + assert.strictEqual(util.getComparisonKey(u1) === util.getComparisonKey(u2), expected, `comparison keys ${u1.toString()}, ${u2.toString()}`); + assert.strictEqual(util.isEqualOrParent(u1, u2), expected, `isEqualOrParent ${u1.toString()}, ${u2.toString()}`); if (!ignoreCase) { - assert.equal(u1.toString() === u2.toString(), expected); + assert.strictEqual(u1.toString() === u2.toString(), expected); } } @@ -402,31 +402,31 @@ suite('Resources', () => { let fileURI = isWindows ? URI.file('c:\\foo\\bar') : URI.file('/foo/bar'); let fileURI2 = isWindows ? URI.file('c:\\foo') : URI.file('/foo'); let fileURI2b = isWindows ? URI.file('C:\\Foo\\') : URI.file('/Foo/'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI), true, '1'); - assert.equal(extUri.isEqualOrParent(fileURI, fileURI), true, '2'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI2), true, '3'); - assert.equal(extUri.isEqualOrParent(fileURI, fileURI2), true, '4'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI2b), true, '5'); - assert.equal(extUri.isEqualOrParent(fileURI, fileURI2b), false, '6'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI), true, '1'); + assert.strictEqual(extUri.isEqualOrParent(fileURI, fileURI), true, '2'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI2), true, '3'); + assert.strictEqual(extUri.isEqualOrParent(fileURI, fileURI2), true, '4'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI2b), true, '5'); + assert.strictEqual(extUri.isEqualOrParent(fileURI, fileURI2b), false, '6'); - assert.equal(extUri.isEqualOrParent(fileURI2, fileURI), false, '7'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI2b, fileURI2), true, '8'); + assert.strictEqual(extUri.isEqualOrParent(fileURI2, fileURI), false, '7'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI2b, fileURI2), true, '8'); let fileURI3 = URI.parse('foo://server:453/foo/bar/goo'); let fileURI4 = URI.parse('foo://server:453/foo/'); let fileURI5 = URI.parse('foo://server:453/foo'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI3, true), true, '11'); - assert.equal(extUri.isEqualOrParent(fileURI3, fileURI3), true, '12'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI4, true), true, '13'); - assert.equal(extUri.isEqualOrParent(fileURI3, fileURI4), true, '14'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI, true), false, '15'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI5, fileURI5, true), true, '16'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI3, true), true, '11'); + assert.strictEqual(extUri.isEqualOrParent(fileURI3, fileURI3), true, '12'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI4, true), true, '13'); + assert.strictEqual(extUri.isEqualOrParent(fileURI3, fileURI4), true, '14'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI, true), false, '15'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI5, fileURI5, true), true, '16'); let fileURI6 = URI.parse('foo://server:453/foo?q=1'); let fileURI7 = URI.parse('foo://server:453/foo/bar?q=1'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI6, fileURI5), false, '17'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI6, fileURI6), true, '18'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI7, fileURI6), true, '19'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI7, fileURI5), false, '20'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI6, fileURI5), false, '17'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI6, fileURI6), true, '18'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI7, fileURI6), true, '19'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI7, fileURI5), false, '20'); }); }); diff --git a/src/vs/base/test/common/scrollable.test.ts b/src/vs/base/test/common/scrollable.test.ts index 820862f2..bf8e6cb0 100644 --- a/src/vs/base/test/common/scrollable.test.ts +++ b/src/vs/base/test/common/scrollable.test.ts @@ -57,7 +57,7 @@ suite('SmoothScrollingOperation', () => { function assertSmoothScroll(from: number, to: number, expected: [number, number][]): void { const actual = simulateSmoothScroll(from, to); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } test('scroll 25 lines (40 fit)', () => { diff --git a/src/vs/base/test/common/skipList.test.ts b/src/vs/base/test/common/skipList.test.ts index f6a083e3..ec3da0d1 100644 --- a/src/vs/base/test/common/skipList.test.ts +++ b/src/vs/base/test/common/skipList.test.ts @@ -12,45 +12,45 @@ import { binarySearch } from 'vs/base/common/arrays'; suite('SkipList', function () { function assertValues(list: SkipList, expected: V[]) { - assert.equal(list.size, expected.length); - assert.deepEqual([...list.values()], expected); + assert.strictEqual(list.size, expected.length); + assert.deepStrictEqual([...list.values()], expected); let valuesFromEntries = [...list.entries()].map(entry => entry[1]); - assert.deepEqual(valuesFromEntries, expected); + assert.deepStrictEqual(valuesFromEntries, expected); let valuesFromIter = [...list].map(entry => entry[1]); - assert.deepEqual(valuesFromIter, expected); + assert.deepStrictEqual(valuesFromIter, expected); let i = 0; list.forEach((value, _key, map) => { assert.ok(map === list); - assert.deepEqual(value, expected[i++]); + assert.deepStrictEqual(value, expected[i++]); }); } function assertKeys(list: SkipList, expected: K[]) { - assert.equal(list.size, expected.length); - assert.deepEqual([...list.keys()], expected); + assert.strictEqual(list.size, expected.length); + assert.deepStrictEqual([...list.keys()], expected); let keysFromEntries = [...list.entries()].map(entry => entry[0]); - assert.deepEqual(keysFromEntries, expected); + assert.deepStrictEqual(keysFromEntries, expected); let keysFromIter = [...list].map(entry => entry[0]); - assert.deepEqual(keysFromIter, expected); + assert.deepStrictEqual(keysFromIter, expected); let i = 0; list.forEach((_value, key, map) => { assert.ok(map === list); - assert.deepEqual(key, expected[i++]); + assert.deepStrictEqual(key, expected[i++]); }); } test('set/get/delete', function () { let list = new SkipList((a, b) => a - b); - assert.equal(list.get(3), undefined); + assert.strictEqual(list.get(3), undefined); list.set(3, 1); - assert.equal(list.get(3), 1); + assert.strictEqual(list.get(3), 1); assertValues(list, [1]); list.set(3, 3); @@ -58,17 +58,17 @@ suite('SkipList', function () { list.set(1, 1); list.set(4, 4); - assert.equal(list.get(3), 3); - assert.equal(list.get(1), 1); - assert.equal(list.get(4), 4); + assert.strictEqual(list.get(3), 3); + assert.strictEqual(list.get(1), 1); + assert.strictEqual(list.get(4), 4); assertValues(list, [1, 3, 4]); - assert.equal(list.delete(17), false); + assert.strictEqual(list.delete(17), false); - assert.equal(list.delete(1), true); - assert.equal(list.get(1), undefined); - assert.equal(list.get(3), 3); - assert.equal(list.get(4), 4); + assert.strictEqual(list.delete(1), true); + assert.strictEqual(list.get(1), undefined); + assert.strictEqual(list.get(3), 3); + assert.strictEqual(list.get(4), 4); assertValues(list, [3, 4]); }); @@ -87,7 +87,7 @@ suite('SkipList', function () { assertKeys(list, [3, 6, 7, 9, 12, 19, 21, 25]); list.set(17, true); - assert.deepEqual(list.size, 9); + assert.deepStrictEqual(list.size, 9); assertKeys(list, [3, 6, 7, 9, 12, 17, 19, 21, 25]); }); @@ -141,8 +141,7 @@ suite('SkipList', function () { } - test('perf', function () { - this.skip(); + test.skip('perf', function () { // data const max = 2 ** 16; diff --git a/src/vs/base/test/common/stream.test.ts b/src/vs/base/test/common/stream.test.ts index 0ef3b773..0fb36467 100644 --- a/src/vs/base/test/common/stream.test.ts +++ b/src/vs/base/test/common/stream.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { isReadableStream, newWriteableStream, Readable, consumeReadable, peekReadable, consumeStream, ReadableStream, toStream, toReadable, transform, peekStream, isReadableBufferedStream } from 'vs/base/common/stream'; +import { isReadableStream, newWriteableStream, Readable, consumeReadable, peekReadable, consumeStream, ReadableStream, toStream, toReadable, transform, peekStream, isReadableBufferedStream, observe } from 'vs/base/common/stream'; import { timeout } from 'vs/base/common/async'; suite('Stream', () => { @@ -43,37 +43,37 @@ suite('Stream', () => { chunks.push(data); }); - assert.equal(chunks[0], 'Hello'); + assert.strictEqual(chunks[0], 'Hello'); stream.write('World'); - assert.equal(chunks[1], 'World'); + assert.strictEqual(chunks[1], 'World'); - assert.equal(error, false); - assert.equal(end, false); + assert.strictEqual(error, false); + assert.strictEqual(end, false); stream.pause(); stream.write('1'); stream.write('2'); stream.write('3'); - assert.equal(chunks.length, 2); + assert.strictEqual(chunks.length, 2); stream.resume(); - assert.equal(chunks.length, 3); - assert.equal(chunks[2], '1,2,3'); + assert.strictEqual(chunks.length, 3); + assert.strictEqual(chunks[2], '1,2,3'); stream.error(new Error()); - assert.equal(error, true); + assert.strictEqual(error, true); stream.end('Final Bit'); - assert.equal(chunks.length, 4); - assert.equal(chunks[3], 'Final Bit'); + assert.strictEqual(chunks.length, 4); + assert.strictEqual(chunks[3], 'Final Bit'); stream.destroy(); stream.write('Unexpected'); - assert.equal(chunks.length, 4); + assert.strictEqual(chunks.length, 4); }); test('WriteableStream - removeListener', () => { @@ -92,22 +92,24 @@ suite('Stream', () => { stream.on('data', dataListener); stream.write('Hello'); - assert.equal(data, true); + assert.strictEqual(data, true); data = false; stream.removeListener('data', dataListener); stream.write('World'); - assert.equal(data, false); + assert.strictEqual(data, false); stream.error(new Error()); - assert.equal(error, true); + assert.strictEqual(error, true); error = false; stream.removeListener('error', errorListener); + // always leave at least one error listener to streams to avoid unexpected errors during test running + stream.on('error', () => { }); stream.error(new Error()); - assert.equal(error, false); + assert.strictEqual(error, false); }); test('WriteableStream - highWaterMark', async () => { @@ -147,14 +149,14 @@ suite('Stream', () => { assert.ok(data); await timeout(0); - assert.equal(drained1, true); - assert.equal(drained2, true); + assert.strictEqual(drained1, true); + assert.strictEqual(drained2, true); }); test('consumeReadable', () => { const readable = arrayToReadable(['1', '2', '3', '4', '5']); const consumed = consumeReadable(readable, strings => strings.join()); - assert.equal(consumed, '1,2,3,4,5'); + assert.strictEqual(consumed, '1,2,3,4,5'); }); test('peekReadable', () => { @@ -166,17 +168,17 @@ suite('Stream', () => { assert.fail('Unexpected result'); } else { const consumed = consumeReadable(consumedOrReadable, strings => strings.join()); - assert.equal(consumed, '1,2,3,4,5'); + assert.strictEqual(consumed, '1,2,3,4,5'); } } let readable = arrayToReadable(['1', '2', '3', '4', '5']); let consumedOrReadable = peekReadable(readable, strings => strings.join(), 5); - assert.equal(consumedOrReadable, '1,2,3,4,5'); + assert.strictEqual(consumedOrReadable, '1,2,3,4,5'); readable = arrayToReadable(['1', '2', '3', '4', '5']); consumedOrReadable = peekReadable(readable, strings => strings.join(), 6); - assert.equal(consumedOrReadable, '1,2,3,4,5'); + assert.strictEqual(consumedOrReadable, '1,2,3,4,5'); }); test('peekReadable - error handling', async () => { @@ -265,7 +267,7 @@ suite('Stream', () => { test('consumeStream', async () => { const stream = readableToStream(arrayToReadable(['1', '2', '3', '4', '5'])); const consumed = await consumeStream(stream, strings => strings.join()); - assert.equal(consumed, '1,2,3,4,5'); + assert.strictEqual(consumed, '1,2,3,4,5'); }); test('peekStream', async () => { @@ -273,11 +275,11 @@ suite('Stream', () => { const stream = readableToStream(arrayToReadable(['1', '2', '3', '4', '5'])); const result = await peekStream(stream, i); - assert.equal(stream, result.stream); + assert.strictEqual(stream, result.stream); if (result.ended) { assert.fail('Unexpected result, stream should not have ended yet'); } else { - assert.equal(result.buffer.length, i + 1, `maxChunks: ${i}`); + assert.strictEqual(result.buffer.length, i + 1, `maxChunks: ${i}`); const additionalResult: string[] = []; await consumeStream(stream, strings => { @@ -286,33 +288,33 @@ suite('Stream', () => { return strings.join(); }); - assert.equal([...result.buffer, ...additionalResult].join(), '1,2,3,4,5'); + assert.strictEqual([...result.buffer, ...additionalResult].join(), '1,2,3,4,5'); } } let stream = readableToStream(arrayToReadable(['1', '2', '3', '4', '5'])); let result = await peekStream(stream, 5); - assert.equal(stream, result.stream); - assert.equal(result.buffer.join(), '1,2,3,4,5'); - assert.equal(result.ended, true); + assert.strictEqual(stream, result.stream); + assert.strictEqual(result.buffer.join(), '1,2,3,4,5'); + assert.strictEqual(result.ended, true); stream = readableToStream(arrayToReadable(['1', '2', '3', '4', '5'])); result = await peekStream(stream, 6); - assert.equal(stream, result.stream); - assert.equal(result.buffer.join(), '1,2,3,4,5'); - assert.equal(result.ended, true); + assert.strictEqual(stream, result.stream); + assert.strictEqual(result.buffer.join(), '1,2,3,4,5'); + assert.strictEqual(result.ended, true); }); test('toStream', async () => { const stream = toStream('1,2,3,4,5', strings => strings.join()); const consumed = await consumeStream(stream, strings => strings.join()); - assert.equal(consumed, '1,2,3,4,5'); + assert.strictEqual(consumed, '1,2,3,4,5'); }); test('toReadable', async () => { const readable = toReadable('1,2,3,4,5'); const consumed = await consumeReadable(readable, strings => strings.join()); - assert.equal(consumed, '1,2,3,4,5'); + assert.strictEqual(consumed, '1,2,3,4,5'); }); test('transform', async () => { @@ -330,6 +332,47 @@ suite('Stream', () => { }, 0); const consumed = await consumeStream(result, strings => strings.join()); - assert.equal(consumed, '11,22,33,44,55'); + assert.strictEqual(consumed, '11,22,33,44,55'); + }); + + test('observer', async () => { + const source1 = newWriteableStream(strings => strings.join()); + setTimeout(() => source1.error(new Error())); + await observe(source1).errorOrEnd(); + + const source2 = newWriteableStream(strings => strings.join()); + setTimeout(() => source2.end('Hello Test')); + await observe(source2).errorOrEnd(); + + const source3 = newWriteableStream(strings => strings.join()); + setTimeout(() => { + source3.write('Hello Test'); + source3.error(new Error()); + }); + await observe(source3).errorOrEnd(); + + const source4 = newWriteableStream(strings => strings.join()); + setTimeout(() => { + source4.write('Hello Test'); + source4.end(); + }); + await observe(source4).errorOrEnd(); + }); + + test('events are delivered even if a listener is removed during delivery', () => { + const stream = newWriteableStream(strings => strings.join()); + + let listener1Called = false; + let listener2Called = false; + + const listener1 = () => { stream.removeListener('end', listener1); listener1Called = true; }; + const listener2 = () => { listener2Called = true; }; + stream.on('end', listener1); + stream.on('end', listener2); + stream.on('data', () => { }); + stream.end(''); + + assert.strictEqual(listener1Called, true); + assert.strictEqual(listener2Called, true); }); }); diff --git a/src/vs/base/test/common/strings.test.ts b/src/vs/base/test/common/strings.test.ts index d3366a1e..dc8d9da5 100644 --- a/src/vs/base/test/common/strings.test.ts +++ b/src/vs/base/test/common/strings.test.ts @@ -52,7 +52,7 @@ suite('Strings', () => { let expected = strings.compare(a.toLowerCase(), b.toLowerCase()); expected = expected > 0 ? 1 : expected < 0 ? -1 : expected; - assert.equal(actual, expected, `${a} <> ${b}`); + assert.strictEqual(actual, expected, `${a} <> ${b}`); if (recurse) { assertCompareIgnoreCase(b, a, false); @@ -89,7 +89,7 @@ suite('Strings', () => { let expected = strings.compare(a.toLowerCase().substring(aStart, aEnd), b.toLowerCase().substring(bStart, bEnd)); expected = expected > 0 ? 1 : expected < 0 ? -1 : expected; - assert.equal(actual, expected, `${a} <> ${b}`); + assert.strictEqual(actual, expected, `${a} <> ${b}`); if (recurse) { assertCompareIgnoreCase(b, a, bStart, bEnd, aStart, aEnd, false); @@ -188,36 +188,36 @@ suite('Strings', () => { }); test('containsRTL', () => { - assert.equal(strings.containsRTL('a'), false); - assert.equal(strings.containsRTL(''), false); - assert.equal(strings.containsRTL(strings.UTF8_BOM_CHARACTER + 'a'), false); - assert.equal(strings.containsRTL('hello world!'), false); - assert.equal(strings.containsRTL('a📚📚b'), false); - assert.equal(strings.containsRTL('هناك حقيقة مثبتة منذ زمن طويل'), true); - assert.equal(strings.containsRTL('זוהי עובדה מבוססת שדעתו'), true); + assert.strictEqual(strings.containsRTL('a'), false); + assert.strictEqual(strings.containsRTL(''), false); + assert.strictEqual(strings.containsRTL(strings.UTF8_BOM_CHARACTER + 'a'), false); + assert.strictEqual(strings.containsRTL('hello world!'), false); + assert.strictEqual(strings.containsRTL('a📚📚b'), false); + assert.strictEqual(strings.containsRTL('هناك حقيقة مثبتة منذ زمن طويل'), true); + assert.strictEqual(strings.containsRTL('זוהי עובדה מבוססת שדעתו'), true); }); test('containsEmoji', () => { - assert.equal(strings.containsEmoji('a'), false); - assert.equal(strings.containsEmoji(''), false); - assert.equal(strings.containsEmoji(strings.UTF8_BOM_CHARACTER + 'a'), false); - assert.equal(strings.containsEmoji('hello world!'), false); - assert.equal(strings.containsEmoji('هناك حقيقة مثبتة منذ زمن طويل'), false); - assert.equal(strings.containsEmoji('זוהי עובדה מבוססת שדעתו'), false); + assert.strictEqual(strings.containsEmoji('a'), false); + assert.strictEqual(strings.containsEmoji(''), false); + assert.strictEqual(strings.containsEmoji(strings.UTF8_BOM_CHARACTER + 'a'), false); + assert.strictEqual(strings.containsEmoji('hello world!'), false); + assert.strictEqual(strings.containsEmoji('هناك حقيقة مثبتة منذ زمن طويل'), false); + assert.strictEqual(strings.containsEmoji('זוהי עובדה מבוססת שדעתו'), false); - assert.equal(strings.containsEmoji('a📚📚b'), true); - assert.equal(strings.containsEmoji('1F600 # 😀 grinning face'), true); - assert.equal(strings.containsEmoji('1F47E # 👾 alien monster'), true); - assert.equal(strings.containsEmoji('1F467 1F3FD # 👧🏽 girl: medium skin tone'), true); - assert.equal(strings.containsEmoji('26EA # ⛪ church'), true); - assert.equal(strings.containsEmoji('231B # ⌛ hourglass'), true); - assert.equal(strings.containsEmoji('2702 # ✂ scissors'), true); - assert.equal(strings.containsEmoji('1F1F7 1F1F4 # 🇷🇴 Romania'), true); + assert.strictEqual(strings.containsEmoji('a📚📚b'), true); + assert.strictEqual(strings.containsEmoji('1F600 # 😀 grinning face'), true); + assert.strictEqual(strings.containsEmoji('1F47E # 👾 alien monster'), true); + assert.strictEqual(strings.containsEmoji('1F467 1F3FD # 👧🏽 girl: medium skin tone'), true); + assert.strictEqual(strings.containsEmoji('26EA # ⛪ church'), true); + assert.strictEqual(strings.containsEmoji('231B # ⌛ hourglass'), true); + assert.strictEqual(strings.containsEmoji('2702 # ✂ scissors'), true); + assert.strictEqual(strings.containsEmoji('1F1F7 1F1F4 # 🇷🇴 Romania'), true); }); test('isBasicASCII', () => { function assertIsBasicASCII(str: string, expected: boolean): void { - assert.equal(strings.isBasicASCII(str), expected, str + ` (${str.charCodeAt(0)})`); + assert.strictEqual(strings.isBasicASCII(str), expected, str + ` (${str.charCodeAt(0)})`); } assertIsBasicASCII('abcdefghijklmnopqrstuvwxyz', true); assertIsBasicASCII('ABCDEFGHIJKLMNOPQRSTUVWXYZ', true); @@ -245,16 +245,16 @@ suite('Strings', () => { assert.throws(() => strings.createRegExp('', false)); // Escapes appropriately - assert.equal(strings.createRegExp('abc', false).source, 'abc'); - assert.equal(strings.createRegExp('([^ ,.]*)', false).source, '\\(\\[\\^ ,\\.\\]\\*\\)'); - assert.equal(strings.createRegExp('([^ ,.]*)', true).source, '([^ ,.]*)'); + assert.strictEqual(strings.createRegExp('abc', false).source, 'abc'); + assert.strictEqual(strings.createRegExp('([^ ,.]*)', false).source, '\\(\\[\\^ ,\\.\\]\\*\\)'); + assert.strictEqual(strings.createRegExp('([^ ,.]*)', true).source, '([^ ,.]*)'); // Whole word - assert.equal(strings.createRegExp('abc', false, { wholeWord: true }).source, '\\babc\\b'); - assert.equal(strings.createRegExp('abc', true, { wholeWord: true }).source, '\\babc\\b'); - assert.equal(strings.createRegExp(' abc', true, { wholeWord: true }).source, ' abc\\b'); - assert.equal(strings.createRegExp('abc ', true, { wholeWord: true }).source, '\\babc '); - assert.equal(strings.createRegExp(' abc ', true, { wholeWord: true }).source, ' abc '); + assert.strictEqual(strings.createRegExp('abc', false, { wholeWord: true }).source, '\\babc\\b'); + assert.strictEqual(strings.createRegExp('abc', true, { wholeWord: true }).source, '\\babc\\b'); + assert.strictEqual(strings.createRegExp(' abc', true, { wholeWord: true }).source, ' abc\\b'); + assert.strictEqual(strings.createRegExp('abc ', true, { wholeWord: true }).source, '\\babc '); + assert.strictEqual(strings.createRegExp(' abc ', true, { wholeWord: true }).source, ' abc '); const regExpWithoutFlags = strings.createRegExp('abc', true); assert(!regExpWithoutFlags.global); @@ -284,15 +284,15 @@ suite('Strings', () => { }); test('getLeadingWhitespace', () => { - assert.equal(strings.getLeadingWhitespace(' foo'), ' '); - assert.equal(strings.getLeadingWhitespace(' foo', 2), ''); - assert.equal(strings.getLeadingWhitespace(' foo', 1, 1), ''); - assert.equal(strings.getLeadingWhitespace(' foo', 0, 1), ' '); - assert.equal(strings.getLeadingWhitespace(' '), ' '); - assert.equal(strings.getLeadingWhitespace(' ', 1), ' '); - assert.equal(strings.getLeadingWhitespace(' ', 0, 1), ' '); - assert.equal(strings.getLeadingWhitespace('\t\tfunction foo(){', 0, 1), '\t'); - assert.equal(strings.getLeadingWhitespace('\t\tfunction foo(){', 0, 2), '\t\t'); + assert.strictEqual(strings.getLeadingWhitespace(' foo'), ' '); + assert.strictEqual(strings.getLeadingWhitespace(' foo', 2), ''); + assert.strictEqual(strings.getLeadingWhitespace(' foo', 1, 1), ''); + assert.strictEqual(strings.getLeadingWhitespace(' foo', 0, 1), ' '); + assert.strictEqual(strings.getLeadingWhitespace(' '), ' '); + assert.strictEqual(strings.getLeadingWhitespace(' ', 1), ' '); + assert.strictEqual(strings.getLeadingWhitespace(' ', 0, 1), ' '); + assert.strictEqual(strings.getLeadingWhitespace('\t\tfunction foo(){', 0, 1), '\t'); + assert.strictEqual(strings.getLeadingWhitespace('\t\tfunction foo(){', 0, 2), '\t\t'); }); test('fuzzyContains', () => { @@ -316,11 +316,11 @@ suite('Strings', () => { }); test('stripUTF8BOM', () => { - assert.equal(strings.stripUTF8BOM(strings.UTF8_BOM_CHARACTER), ''); - assert.equal(strings.stripUTF8BOM(strings.UTF8_BOM_CHARACTER + 'foobar'), 'foobar'); - assert.equal(strings.stripUTF8BOM('foobar' + strings.UTF8_BOM_CHARACTER), 'foobar' + strings.UTF8_BOM_CHARACTER); - assert.equal(strings.stripUTF8BOM('abc'), 'abc'); - assert.equal(strings.stripUTF8BOM(''), ''); + assert.strictEqual(strings.stripUTF8BOM(strings.UTF8_BOM_CHARACTER), ''); + assert.strictEqual(strings.stripUTF8BOM(strings.UTF8_BOM_CHARACTER + 'foobar'), 'foobar'); + assert.strictEqual(strings.stripUTF8BOM('foobar' + strings.UTF8_BOM_CHARACTER), 'foobar' + strings.UTF8_BOM_CHARACTER); + assert.strictEqual(strings.stripUTF8BOM('abc'), 'abc'); + assert.strictEqual(strings.stripUTF8BOM(''), ''); }); test('containsUppercaseCharacter', () => { @@ -340,7 +340,7 @@ suite('Strings', () => { ['FöÖ', true], ['\\Foo', true], ].forEach(([str, result]) => { - assert.equal(strings.containsUppercaseCharacter(str), result, `Wrong result for ${str}`); + assert.strictEqual(strings.containsUppercaseCharacter(str), result, `Wrong result for ${str}`); }); }); @@ -352,7 +352,7 @@ suite('Strings', () => { ['Foo', true], ].forEach(([str, result]) => { - assert.equal(strings.containsUppercaseCharacter(str, true), result, `Wrong result for ${str}`); + assert.strictEqual(strings.containsUppercaseCharacter(str, true), result, `Wrong result for ${str}`); }); }); @@ -364,20 +364,20 @@ suite('Strings', () => { ['123', '123'], ['.a', '.a'], ].forEach(([inStr, result]) => { - assert.equal(strings.uppercaseFirstLetter(inStr), result, `Wrong result for ${inStr}`); + assert.strictEqual(strings.uppercaseFirstLetter(inStr), result, `Wrong result for ${inStr}`); }); }); test('getNLines', () => { - assert.equal(strings.getNLines('', 5), ''); - assert.equal(strings.getNLines('foo', 5), 'foo'); - assert.equal(strings.getNLines('foo\nbar', 5), 'foo\nbar'); - assert.equal(strings.getNLines('foo\nbar', 2), 'foo\nbar'); + assert.strictEqual(strings.getNLines('', 5), ''); + assert.strictEqual(strings.getNLines('foo', 5), 'foo'); + assert.strictEqual(strings.getNLines('foo\nbar', 5), 'foo\nbar'); + assert.strictEqual(strings.getNLines('foo\nbar', 2), 'foo\nbar'); - assert.equal(strings.getNLines('foo\nbar', 1), 'foo'); - assert.equal(strings.getNLines('foo\nbar'), 'foo'); - assert.equal(strings.getNLines('foo\nbar\nsomething', 2), 'foo\nbar'); - assert.equal(strings.getNLines('foo', 0), ''); + assert.strictEqual(strings.getNLines('foo\nbar', 1), 'foo'); + assert.strictEqual(strings.getNLines('foo\nbar'), 'foo'); + assert.strictEqual(strings.getNLines('foo\nbar\nsomething', 2), 'foo\nbar'); + assert.strictEqual(strings.getNLines('foo', 0), ''); }); test('encodeUTF8', function () { @@ -387,12 +387,12 @@ suite('Strings', () => { for (let offset = 0; offset < actual.byteLength; offset++) { actualArr[offset] = actual[offset]; } - assert.deepEqual(actualArr, expected); + assert.deepStrictEqual(actualArr, expected); } function assertDecodeUTF8(data: number[], expected: string): void { const actual = strings.decodeUTF8(new Uint8Array(data)); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } function assertEncodeDecodeUTF8(str: string, buff: number[]): void { @@ -415,11 +415,11 @@ suite('Strings', () => { }); test('getGraphemeBreakType', () => { - assert.equal(strings.getGraphemeBreakType(0xBC1), strings.GraphemeBreakType.SpacingMark); + assert.strictEqual(strings.getGraphemeBreakType(0xBC1), strings.GraphemeBreakType.SpacingMark); }); test('truncate', () => { - assert.equal('hello world', strings.truncate('hello world', 100)); - assert.equal('hello…', strings.truncate('hello world', 5)); + assert.strictEqual('hello world', strings.truncate('hello world', 100)); + assert.strictEqual('hello…', strings.truncate('hello world', 5)); }); }); diff --git a/src/vs/base/test/common/troubleshooting.ts b/src/vs/base/test/common/troubleshooting.ts new file mode 100644 index 00000000..1d9d9963 --- /dev/null +++ b/src/vs/base/test/common/troubleshooting.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable, IDisposableTracker, setDisposableTracker } from 'vs/base/common/lifecycle'; + +class DisposableTracker implements IDisposableTracker { + allDisposables: [IDisposable, string][] = []; + trackDisposable(x: IDisposable): void { + this.allDisposables.push([x, new Error().stack!]); + } + markTracked(x: IDisposable): void { + for (let idx = 0; idx < this.allDisposables.length; idx++) { + if (this.allDisposables[idx][0] === x) { + this.allDisposables.splice(idx, 1); + return; + } + } + } +} + +let currentTracker: DisposableTracker | null = null; + +export function beginTrackingDisposables(): void { + currentTracker = new DisposableTracker(); + setDisposableTracker(currentTracker); +} + +export function endTrackingDisposables(): void { + if (currentTracker) { + setDisposableTracker(null); + console.log(currentTracker!.allDisposables.map(e => `${e[0]}\n${e[1]}`).join('\n\n')); + currentTracker = null; + } +} + +export function beginLoggingFS(withStacks: boolean = false): void { + if ((self).beginLoggingFS) { + (self).beginLoggingFS(withStacks); + } +} + +export function endLoggingFS(): void { + if ((self).endLoggingFS) { + (self).endLoggingFS(); + } +} diff --git a/src/vs/base/test/common/types.test.ts b/src/vs/base/test/common/types.test.ts index 0bec27fc..cb7dedbe 100644 --- a/src/vs/base/test/common/types.test.ts +++ b/src/vs/base/test/common/types.test.ts @@ -57,7 +57,7 @@ suite('Types', () => { assert(!types.isObject(/test/)); assert(!types.isObject(new RegExp(''))); assert(!types.isFunction(new Date())); - assert(!types.isObject(assert)); + assert.strictEqual(types.isObject(assert), false); assert(!types.isObject(function foo() { })); assert(types.isObject({})); @@ -75,7 +75,7 @@ suite('Types', () => { assert(!types.isEmptyObject(/test/)); assert(!types.isEmptyObject(new RegExp(''))); assert(!types.isEmptyObject(new Date())); - assert(!types.isEmptyObject(assert)); + assert.strictEqual(types.isEmptyObject(assert), false); assert(!types.isEmptyObject(function foo() { /**/ })); assert(!types.isEmptyObject({ foo: 'bar' })); @@ -178,15 +178,15 @@ suite('Types', () => { assert.throws(() => types.assertAllDefined(true, undefined)); assert.throws(() => types.assertAllDefined(undefined, false)); - assert.equal(types.assertIsDefined(true), true); - assert.equal(types.assertIsDefined(false), false); - assert.equal(types.assertIsDefined('Hello'), 'Hello'); - assert.equal(types.assertIsDefined(''), ''); + assert.strictEqual(types.assertIsDefined(true), true); + assert.strictEqual(types.assertIsDefined(false), false); + assert.strictEqual(types.assertIsDefined('Hello'), 'Hello'); + assert.strictEqual(types.assertIsDefined(''), ''); const res = types.assertAllDefined(1, true, 'Hello'); - assert.equal(res[0], 1); - assert.equal(res[1], true); - assert.equal(res[2], 'Hello'); + assert.strictEqual(res[0], 1); + assert.strictEqual(res[1], true); + assert.strictEqual(res[2], 'Hello'); }); test('validateConstraints', () => { diff --git a/src/vs/base/test/common/uri.test.ts b/src/vs/base/test/common/uri.test.ts index bc4e409c..a05e4fc3 100644 --- a/src/vs/base/test/common/uri.test.ts +++ b/src/vs/base/test/common/uri.test.ts @@ -9,82 +9,82 @@ import { isWindows } from 'vs/base/common/platform'; suite('URI', () => { test('file#toString', () => { - assert.equal(URI.file('c:/win/path').toString(), 'file:///c%3A/win/path'); - assert.equal(URI.file('C:/win/path').toString(), 'file:///c%3A/win/path'); - assert.equal(URI.file('c:/win/path/').toString(), 'file:///c%3A/win/path/'); - assert.equal(URI.file('/c:/win/path').toString(), 'file:///c%3A/win/path'); + assert.strictEqual(URI.file('c:/win/path').toString(), 'file:///c%3A/win/path'); + assert.strictEqual(URI.file('C:/win/path').toString(), 'file:///c%3A/win/path'); + assert.strictEqual(URI.file('c:/win/path/').toString(), 'file:///c%3A/win/path/'); + assert.strictEqual(URI.file('/c:/win/path').toString(), 'file:///c%3A/win/path'); }); test('URI.file (win-special)', () => { if (isWindows) { - assert.equal(URI.file('c:\\win\\path').toString(), 'file:///c%3A/win/path'); - assert.equal(URI.file('c:\\win/path').toString(), 'file:///c%3A/win/path'); + assert.strictEqual(URI.file('c:\\win\\path').toString(), 'file:///c%3A/win/path'); + assert.strictEqual(URI.file('c:\\win/path').toString(), 'file:///c%3A/win/path'); } else { - assert.equal(URI.file('c:\\win\\path').toString(), 'file:///c%3A%5Cwin%5Cpath'); - assert.equal(URI.file('c:\\win/path').toString(), 'file:///c%3A%5Cwin/path'); + assert.strictEqual(URI.file('c:\\win\\path').toString(), 'file:///c%3A%5Cwin%5Cpath'); + assert.strictEqual(URI.file('c:\\win/path').toString(), 'file:///c%3A%5Cwin/path'); } }); test('file#fsPath (win-special)', () => { if (isWindows) { - assert.equal(URI.file('c:\\win\\path').fsPath, 'c:\\win\\path'); - assert.equal(URI.file('c:\\win/path').fsPath, 'c:\\win\\path'); + assert.strictEqual(URI.file('c:\\win\\path').fsPath, 'c:\\win\\path'); + assert.strictEqual(URI.file('c:\\win/path').fsPath, 'c:\\win\\path'); - assert.equal(URI.file('c:/win/path').fsPath, 'c:\\win\\path'); - assert.equal(URI.file('c:/win/path/').fsPath, 'c:\\win\\path\\'); - assert.equal(URI.file('C:/win/path').fsPath, 'c:\\win\\path'); - assert.equal(URI.file('/c:/win/path').fsPath, 'c:\\win\\path'); - assert.equal(URI.file('./c/win/path').fsPath, '\\.\\c\\win\\path'); + assert.strictEqual(URI.file('c:/win/path').fsPath, 'c:\\win\\path'); + assert.strictEqual(URI.file('c:/win/path/').fsPath, 'c:\\win\\path\\'); + assert.strictEqual(URI.file('C:/win/path').fsPath, 'c:\\win\\path'); + assert.strictEqual(URI.file('/c:/win/path').fsPath, 'c:\\win\\path'); + assert.strictEqual(URI.file('./c/win/path').fsPath, '\\.\\c\\win\\path'); } else { - assert.equal(URI.file('c:/win/path').fsPath, 'c:/win/path'); - assert.equal(URI.file('c:/win/path/').fsPath, 'c:/win/path/'); - assert.equal(URI.file('C:/win/path').fsPath, 'c:/win/path'); - assert.equal(URI.file('/c:/win/path').fsPath, 'c:/win/path'); - assert.equal(URI.file('./c/win/path').fsPath, '/./c/win/path'); + assert.strictEqual(URI.file('c:/win/path').fsPath, 'c:/win/path'); + assert.strictEqual(URI.file('c:/win/path/').fsPath, 'c:/win/path/'); + assert.strictEqual(URI.file('C:/win/path').fsPath, 'c:/win/path'); + assert.strictEqual(URI.file('/c:/win/path').fsPath, 'c:/win/path'); + assert.strictEqual(URI.file('./c/win/path').fsPath, '/./c/win/path'); } }); test('URI#fsPath - no `fsPath` when no `path`', () => { const value = URI.parse('file://%2Fhome%2Fticino%2Fdesktop%2Fcpluscplus%2Ftest.cpp'); - assert.equal(value.authority, '/home/ticino/desktop/cpluscplus/test.cpp'); - assert.equal(value.path, '/'); + assert.strictEqual(value.authority, '/home/ticino/desktop/cpluscplus/test.cpp'); + assert.strictEqual(value.path, '/'); if (isWindows) { - assert.equal(value.fsPath, '\\'); + assert.strictEqual(value.fsPath, '\\'); } else { - assert.equal(value.fsPath, '/'); + assert.strictEqual(value.fsPath, '/'); } }); test('http#toString', () => { - assert.equal(URI.from({ scheme: 'http', authority: 'www.msft.com', path: '/my/path' }).toString(), 'http://www.msft.com/my/path'); - assert.equal(URI.from({ scheme: 'http', authority: 'www.msft.com', path: '/my/path' }).toString(), 'http://www.msft.com/my/path'); - assert.equal(URI.from({ scheme: 'http', authority: 'www.MSFT.com', path: '/my/path' }).toString(), 'http://www.msft.com/my/path'); - assert.equal(URI.from({ scheme: 'http', authority: '', path: 'my/path' }).toString(), 'http:/my/path'); - assert.equal(URI.from({ scheme: 'http', authority: '', path: '/my/path' }).toString(), 'http:/my/path'); + assert.strictEqual(URI.from({ scheme: 'http', authority: 'www.msft.com', path: '/my/path' }).toString(), 'http://www.msft.com/my/path'); + assert.strictEqual(URI.from({ scheme: 'http', authority: 'www.msft.com', path: '/my/path' }).toString(), 'http://www.msft.com/my/path'); + assert.strictEqual(URI.from({ scheme: 'http', authority: 'www.MSFT.com', path: '/my/path' }).toString(), 'http://www.msft.com/my/path'); + assert.strictEqual(URI.from({ scheme: 'http', authority: '', path: 'my/path' }).toString(), 'http:/my/path'); + assert.strictEqual(URI.from({ scheme: 'http', authority: '', path: '/my/path' }).toString(), 'http:/my/path'); //http://a-test-site.com/#test=true - assert.equal(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: 'test=true' }).toString(), 'http://a-test-site.com/?test%3Dtrue'); - assert.equal(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: '', fragment: 'test=true' }).toString(), 'http://a-test-site.com/#test%3Dtrue'); + assert.strictEqual(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: 'test=true' }).toString(), 'http://a-test-site.com/?test%3Dtrue'); + assert.strictEqual(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: '', fragment: 'test=true' }).toString(), 'http://a-test-site.com/#test%3Dtrue'); }); test('http#toString, encode=FALSE', () => { - assert.equal(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: 'test=true' }).toString(true), 'http://a-test-site.com/?test=true'); - assert.equal(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: '', fragment: 'test=true' }).toString(true), 'http://a-test-site.com/#test=true'); - assert.equal(URI.from({ scheme: 'http', path: '/api/files/test.me', query: 't=1234' }).toString(true), 'http:/api/files/test.me?t=1234'); + assert.strictEqual(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: 'test=true' }).toString(true), 'http://a-test-site.com/?test=true'); + assert.strictEqual(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: '', fragment: 'test=true' }).toString(true), 'http://a-test-site.com/#test=true'); + assert.strictEqual(URI.from({ scheme: 'http', path: '/api/files/test.me', query: 't=1234' }).toString(true), 'http:/api/files/test.me?t=1234'); const value = URI.parse('file://shares/pröjects/c%23/#l12'); - assert.equal(value.authority, 'shares'); - assert.equal(value.path, '/pröjects/c#/'); - assert.equal(value.fragment, 'l12'); - assert.equal(value.toString(), 'file://shares/pr%C3%B6jects/c%23/#l12'); - assert.equal(value.toString(true), 'file://shares/pröjects/c%23/#l12'); + assert.strictEqual(value.authority, 'shares'); + assert.strictEqual(value.path, '/pröjects/c#/'); + assert.strictEqual(value.fragment, 'l12'); + assert.strictEqual(value.toString(), 'file://shares/pr%C3%B6jects/c%23/#l12'); + assert.strictEqual(value.toString(true), 'file://shares/pröjects/c%23/#l12'); const uri2 = URI.parse(value.toString(true)); const uri3 = URI.parse(value.toString()); - assert.equal(uri2.authority, uri3.authority); - assert.equal(uri2.path, uri3.path); - assert.equal(uri2.query, uri3.query); - assert.equal(uri2.fragment, uri3.fragment); + assert.strictEqual(uri2.authority, uri3.authority); + assert.strictEqual(uri2.path, uri3.path); + assert.strictEqual(uri2.query, uri3.query); + assert.strictEqual(uri2.fragment, uri3.fragment); }); test('with, identity', () => { @@ -101,23 +101,23 @@ suite('URI', () => { }); test('with, changes', () => { - assert.equal(URI.parse('before:some/file/path').with({ scheme: 'after' }).toString(), 'after:some/file/path'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'http', path: '/api/files/test.me', query: 't=1234' }).toString(), 'http:/api/files/test.me?t%3D1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'http', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'http:/api/files/test.me?t%3D1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'https', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'https:/api/files/test.me?t%3D1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'HTTP', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'HTTP:/api/files/test.me?t%3D1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'HTTPS', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'HTTPS:/api/files/test.me?t%3D1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'boo', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'boo:/api/files/test.me?t%3D1234'); + assert.strictEqual(URI.parse('before:some/file/path').with({ scheme: 'after' }).toString(), 'after:some/file/path'); + assert.strictEqual(URI.from({ scheme: 's' }).with({ scheme: 'http', path: '/api/files/test.me', query: 't=1234' }).toString(), 'http:/api/files/test.me?t%3D1234'); + assert.strictEqual(URI.from({ scheme: 's' }).with({ scheme: 'http', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'http:/api/files/test.me?t%3D1234'); + assert.strictEqual(URI.from({ scheme: 's' }).with({ scheme: 'https', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'https:/api/files/test.me?t%3D1234'); + assert.strictEqual(URI.from({ scheme: 's' }).with({ scheme: 'HTTP', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'HTTP:/api/files/test.me?t%3D1234'); + assert.strictEqual(URI.from({ scheme: 's' }).with({ scheme: 'HTTPS', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'HTTPS:/api/files/test.me?t%3D1234'); + assert.strictEqual(URI.from({ scheme: 's' }).with({ scheme: 'boo', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'boo:/api/files/test.me?t%3D1234'); }); test('with, remove components #8465', () => { - assert.equal(URI.parse('scheme://authority/path').with({ authority: '' }).toString(), 'scheme:/path'); - assert.equal(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ authority: '' }).toString(), 'scheme:/path'); - assert.equal(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ authority: null }).toString(), 'scheme:/path'); - assert.equal(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ path: '' }).toString(), 'scheme://authority'); - assert.equal(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ path: null }).toString(), 'scheme://authority'); - assert.equal(URI.parse('scheme:/path').with({ authority: '' }).toString(), 'scheme:/path'); - assert.equal(URI.parse('scheme:/path').with({ authority: null }).toString(), 'scheme:/path'); + assert.strictEqual(URI.parse('scheme://authority/path').with({ authority: '' }).toString(), 'scheme:/path'); + assert.strictEqual(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ authority: '' }).toString(), 'scheme:/path'); + assert.strictEqual(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ authority: null }).toString(), 'scheme:/path'); + assert.strictEqual(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ path: '' }).toString(), 'scheme://authority'); + assert.strictEqual(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ path: null }).toString(), 'scheme://authority'); + assert.strictEqual(URI.parse('scheme:/path').with({ authority: '' }).toString(), 'scheme:/path'); + assert.strictEqual(URI.parse('scheme:/path').with({ authority: null }).toString(), 'scheme:/path'); }); test('with, validation', () => { @@ -130,104 +130,104 @@ suite('URI', () => { test('parse', () => { let value = URI.parse('http:/api/files/test.me?t=1234'); - assert.equal(value.scheme, 'http'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/api/files/test.me'); - assert.equal(value.query, 't=1234'); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'http'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/api/files/test.me'); + assert.strictEqual(value.query, 't=1234'); + assert.strictEqual(value.fragment, ''); value = URI.parse('http://api/files/test.me?t=1234'); - assert.equal(value.scheme, 'http'); - assert.equal(value.authority, 'api'); - assert.equal(value.path, '/files/test.me'); - assert.equal(value.query, 't=1234'); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'http'); + assert.strictEqual(value.authority, 'api'); + assert.strictEqual(value.path, '/files/test.me'); + assert.strictEqual(value.query, 't=1234'); + assert.strictEqual(value.fragment, ''); value = URI.parse('file:///c:/test/me'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/c:/test/me'); - assert.equal(value.fragment, ''); - assert.equal(value.query, ''); - assert.equal(value.fsPath, isWindows ? 'c:\\test\\me' : 'c:/test/me'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/c:/test/me'); + assert.strictEqual(value.fragment, ''); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fsPath, isWindows ? 'c:\\test\\me' : 'c:/test/me'); value = URI.parse('file://shares/files/c%23/p.cs'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, 'shares'); - assert.equal(value.path, '/files/c#/p.cs'); - assert.equal(value.fragment, ''); - assert.equal(value.query, ''); - assert.equal(value.fsPath, isWindows ? '\\\\shares\\files\\c#\\p.cs' : '//shares/files/c#/p.cs'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, 'shares'); + assert.strictEqual(value.path, '/files/c#/p.cs'); + assert.strictEqual(value.fragment, ''); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fsPath, isWindows ? '\\\\shares\\files\\c#\\p.cs' : '//shares/files/c#/p.cs'); value = URI.parse('file:///c:/Source/Z%C3%BCrich%20or%20Zurich%20(%CB%88zj%CA%8A%C9%99r%C9%AAk,/Code/resources/app/plugins/c%23/plugin.json'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/c:/Source/Zürich or Zurich (ˈzjʊərɪk,/Code/resources/app/plugins/c#/plugin.json'); - assert.equal(value.fragment, ''); - assert.equal(value.query, ''); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/c:/Source/Zürich or Zurich (ˈzjʊərɪk,/Code/resources/app/plugins/c#/plugin.json'); + assert.strictEqual(value.fragment, ''); + assert.strictEqual(value.query, ''); value = URI.parse('file:///c:/test %25/path'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/c:/test %/path'); - assert.equal(value.fragment, ''); - assert.equal(value.query, ''); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/c:/test %/path'); + assert.strictEqual(value.fragment, ''); + assert.strictEqual(value.query, ''); value = URI.parse('inmemory:'); - assert.equal(value.scheme, 'inmemory'); - assert.equal(value.authority, ''); - assert.equal(value.path, ''); - assert.equal(value.query, ''); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'inmemory'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, ''); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, ''); value = URI.parse('foo:api/files/test'); - assert.equal(value.scheme, 'foo'); - assert.equal(value.authority, ''); - assert.equal(value.path, 'api/files/test'); - assert.equal(value.query, ''); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'foo'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, 'api/files/test'); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, ''); value = URI.parse('file:?q'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/'); - assert.equal(value.query, 'q'); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/'); + assert.strictEqual(value.query, 'q'); + assert.strictEqual(value.fragment, ''); value = URI.parse('file:#d'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/'); - assert.equal(value.query, ''); - assert.equal(value.fragment, 'd'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/'); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, 'd'); value = URI.parse('f3ile:#d'); - assert.equal(value.scheme, 'f3ile'); - assert.equal(value.authority, ''); - assert.equal(value.path, ''); - assert.equal(value.query, ''); - assert.equal(value.fragment, 'd'); + assert.strictEqual(value.scheme, 'f3ile'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, ''); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, 'd'); value = URI.parse('foo+bar:path'); - assert.equal(value.scheme, 'foo+bar'); - assert.equal(value.authority, ''); - assert.equal(value.path, 'path'); - assert.equal(value.query, ''); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'foo+bar'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, 'path'); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, ''); value = URI.parse('foo-bar:path'); - assert.equal(value.scheme, 'foo-bar'); - assert.equal(value.authority, ''); - assert.equal(value.path, 'path'); - assert.equal(value.query, ''); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'foo-bar'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, 'path'); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, ''); value = URI.parse('foo.bar:path'); - assert.equal(value.scheme, 'foo.bar'); - assert.equal(value.authority, ''); - assert.equal(value.path, 'path'); - assert.equal(value.query, ''); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'foo.bar'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, 'path'); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, ''); }); test('parse, disallow //path when no authority', () => { @@ -237,132 +237,132 @@ suite('URI', () => { test('URI#file, win-speciale', () => { if (isWindows) { let value = URI.file('c:\\test\\drive'); - assert.equal(value.path, '/c:/test/drive'); - assert.equal(value.toString(), 'file:///c%3A/test/drive'); + assert.strictEqual(value.path, '/c:/test/drive'); + assert.strictEqual(value.toString(), 'file:///c%3A/test/drive'); value = URI.file('\\\\shäres\\path\\c#\\plugin.json'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, 'shäres'); - assert.equal(value.path, '/path/c#/plugin.json'); - assert.equal(value.fragment, ''); - assert.equal(value.query, ''); - assert.equal(value.toString(), 'file://sh%C3%A4res/path/c%23/plugin.json'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, 'shäres'); + assert.strictEqual(value.path, '/path/c#/plugin.json'); + assert.strictEqual(value.fragment, ''); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.toString(), 'file://sh%C3%A4res/path/c%23/plugin.json'); value = URI.file('\\\\localhost\\c$\\GitDevelopment\\express'); - assert.equal(value.scheme, 'file'); - assert.equal(value.path, '/c$/GitDevelopment/express'); - assert.equal(value.fsPath, '\\\\localhost\\c$\\GitDevelopment\\express'); - assert.equal(value.query, ''); - assert.equal(value.fragment, ''); - assert.equal(value.toString(), 'file://localhost/c%24/GitDevelopment/express'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.path, '/c$/GitDevelopment/express'); + assert.strictEqual(value.fsPath, '\\\\localhost\\c$\\GitDevelopment\\express'); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, ''); + assert.strictEqual(value.toString(), 'file://localhost/c%24/GitDevelopment/express'); value = URI.file('c:\\test with %\\path'); - assert.equal(value.path, '/c:/test with %/path'); - assert.equal(value.toString(), 'file:///c%3A/test%20with%20%25/path'); + assert.strictEqual(value.path, '/c:/test with %/path'); + assert.strictEqual(value.toString(), 'file:///c%3A/test%20with%20%25/path'); value = URI.file('c:\\test with %25\\path'); - assert.equal(value.path, '/c:/test with %25/path'); - assert.equal(value.toString(), 'file:///c%3A/test%20with%20%2525/path'); + assert.strictEqual(value.path, '/c:/test with %25/path'); + assert.strictEqual(value.toString(), 'file:///c%3A/test%20with%20%2525/path'); value = URI.file('c:\\test with %25\\c#code'); - assert.equal(value.path, '/c:/test with %25/c#code'); - assert.equal(value.toString(), 'file:///c%3A/test%20with%20%2525/c%23code'); + assert.strictEqual(value.path, '/c:/test with %25/c#code'); + assert.strictEqual(value.toString(), 'file:///c%3A/test%20with%20%2525/c%23code'); value = URI.file('\\\\shares'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, 'shares'); - assert.equal(value.path, '/'); // slash is always there + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, 'shares'); + assert.strictEqual(value.path, '/'); // slash is always there value = URI.file('\\\\shares\\'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, 'shares'); - assert.equal(value.path, '/'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, 'shares'); + assert.strictEqual(value.path, '/'); } }); test('VSCode URI module\'s driveLetterPath regex is incorrect, #32961', function () { let uri = URI.parse('file:///_:/path'); - assert.equal(uri.fsPath, isWindows ? '\\_:\\path' : '/_:/path'); + assert.strictEqual(uri.fsPath, isWindows ? '\\_:\\path' : '/_:/path'); }); test('URI#file, no path-is-uri check', () => { // we don't complain here let value = URI.file('file://path/to/file'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/file://path/to/file'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/file://path/to/file'); }); test('URI#file, always slash', () => { let value = URI.file('a.file'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/a.file'); - assert.equal(value.toString(), 'file:///a.file'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/a.file'); + assert.strictEqual(value.toString(), 'file:///a.file'); value = URI.parse(value.toString()); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/a.file'); - assert.equal(value.toString(), 'file:///a.file'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/a.file'); + assert.strictEqual(value.toString(), 'file:///a.file'); }); test('URI.toString, only scheme and query', () => { const value = URI.parse('stuff:?qüery'); - assert.equal(value.toString(), 'stuff:?q%C3%BCery'); + assert.strictEqual(value.toString(), 'stuff:?q%C3%BCery'); }); test('URI#toString, upper-case percent espaces', () => { const value = URI.parse('file://sh%c3%a4res/path'); - assert.equal(value.toString(), 'file://sh%C3%A4res/path'); + assert.strictEqual(value.toString(), 'file://sh%C3%A4res/path'); }); test('URI#toString, lower-case windows drive letter', () => { - assert.equal(URI.parse('untitled:c:/Users/jrieken/Code/abc.txt').toString(), 'untitled:c%3A/Users/jrieken/Code/abc.txt'); - assert.equal(URI.parse('untitled:C:/Users/jrieken/Code/abc.txt').toString(), 'untitled:c%3A/Users/jrieken/Code/abc.txt'); + assert.strictEqual(URI.parse('untitled:c:/Users/jrieken/Code/abc.txt').toString(), 'untitled:c%3A/Users/jrieken/Code/abc.txt'); + assert.strictEqual(URI.parse('untitled:C:/Users/jrieken/Code/abc.txt').toString(), 'untitled:c%3A/Users/jrieken/Code/abc.txt'); }); test('URI#toString, escape all the bits', () => { const value = URI.file('/Users/jrieken/Code/_samples/18500/Mödel + Other Thîngß/model.js'); - assert.equal(value.toString(), 'file:///Users/jrieken/Code/_samples/18500/M%C3%B6del%20%2B%20Other%20Th%C3%AEng%C3%9F/model.js'); + assert.strictEqual(value.toString(), 'file:///Users/jrieken/Code/_samples/18500/M%C3%B6del%20%2B%20Other%20Th%C3%AEng%C3%9F/model.js'); }); test('URI#toString, don\'t encode port', () => { let value = URI.parse('http://localhost:8080/far'); - assert.equal(value.toString(), 'http://localhost:8080/far'); + assert.strictEqual(value.toString(), 'http://localhost:8080/far'); value = URI.from({ scheme: 'http', authority: 'löcalhost:8080', path: '/far', query: undefined, fragment: undefined }); - assert.equal(value.toString(), 'http://l%C3%B6calhost:8080/far'); + assert.strictEqual(value.toString(), 'http://l%C3%B6calhost:8080/far'); }); test('URI#toString, user information in authority', () => { let value = URI.parse('http://foo:bar@localhost/far'); - assert.equal(value.toString(), 'http://foo:bar@localhost/far'); + assert.strictEqual(value.toString(), 'http://foo:bar@localhost/far'); value = URI.parse('http://foo@localhost/far'); - assert.equal(value.toString(), 'http://foo@localhost/far'); + assert.strictEqual(value.toString(), 'http://foo@localhost/far'); value = URI.parse('http://foo:bAr@localhost:8080/far'); - assert.equal(value.toString(), 'http://foo:bAr@localhost:8080/far'); + assert.strictEqual(value.toString(), 'http://foo:bAr@localhost:8080/far'); value = URI.parse('http://foo@localhost:8080/far'); - assert.equal(value.toString(), 'http://foo@localhost:8080/far'); + assert.strictEqual(value.toString(), 'http://foo@localhost:8080/far'); value = URI.from({ scheme: 'http', authority: 'föö:bör@löcalhost:8080', path: '/far', query: undefined, fragment: undefined }); - assert.equal(value.toString(), 'http://f%C3%B6%C3%B6:b%C3%B6r@l%C3%B6calhost:8080/far'); + assert.strictEqual(value.toString(), 'http://f%C3%B6%C3%B6:b%C3%B6r@l%C3%B6calhost:8080/far'); }); test('correctFileUriToFilePath2', () => { const test = (input: string, expected: string) => { const value = URI.parse(input); - assert.equal(value.fsPath, expected, 'Result for ' + input); + assert.strictEqual(value.fsPath, expected, 'Result for ' + input); const value2 = URI.file(value.fsPath); - assert.equal(value2.fsPath, expected, 'Result for ' + input); - assert.equal(value.toString(), value2.toString()); + assert.strictEqual(value2.fsPath, expected, 'Result for ' + input); + assert.strictEqual(value.toString(), value2.toString()); }; test('file:///c:/alex.txt', isWindows ? 'c:\\alex.txt' : 'c:/alex.txt'); @@ -374,104 +374,132 @@ suite('URI', () => { test('URI - http, query & toString', function () { let uri = URI.parse('https://go.microsoft.com/fwlink/?LinkId=518008'); - assert.equal(uri.query, 'LinkId=518008'); - assert.equal(uri.toString(true), 'https://go.microsoft.com/fwlink/?LinkId=518008'); - assert.equal(uri.toString(), 'https://go.microsoft.com/fwlink/?LinkId%3D518008'); + assert.strictEqual(uri.query, 'LinkId=518008'); + assert.strictEqual(uri.toString(true), 'https://go.microsoft.com/fwlink/?LinkId=518008'); + assert.strictEqual(uri.toString(), 'https://go.microsoft.com/fwlink/?LinkId%3D518008'); let uri2 = URI.parse(uri.toString()); - assert.equal(uri2.query, 'LinkId=518008'); - assert.equal(uri2.query, uri.query); + assert.strictEqual(uri2.query, 'LinkId=518008'); + assert.strictEqual(uri2.query, uri.query); uri = URI.parse('https://go.microsoft.com/fwlink/?LinkId=518008&foö&ké¥=üü'); - assert.equal(uri.query, 'LinkId=518008&foö&ké¥=üü'); - assert.equal(uri.toString(true), 'https://go.microsoft.com/fwlink/?LinkId=518008&foö&ké¥=üü'); - assert.equal(uri.toString(), 'https://go.microsoft.com/fwlink/?LinkId%3D518008%26fo%C3%B6%26k%C3%A9%C2%A5%3D%C3%BC%C3%BC'); + assert.strictEqual(uri.query, 'LinkId=518008&foö&ké¥=üü'); + assert.strictEqual(uri.toString(true), 'https://go.microsoft.com/fwlink/?LinkId=518008&foö&ké¥=üü'); + assert.strictEqual(uri.toString(), 'https://go.microsoft.com/fwlink/?LinkId%3D518008%26fo%C3%B6%26k%C3%A9%C2%A5%3D%C3%BC%C3%BC'); uri2 = URI.parse(uri.toString()); - assert.equal(uri2.query, 'LinkId=518008&foö&ké¥=üü'); - assert.equal(uri2.query, uri.query); + assert.strictEqual(uri2.query, 'LinkId=518008&foö&ké¥=üü'); + assert.strictEqual(uri2.query, uri.query); // #24849 uri = URI.parse('https://twitter.com/search?src=typd&q=%23tag'); - assert.equal(uri.toString(true), 'https://twitter.com/search?src=typd&q=%23tag'); + assert.strictEqual(uri.toString(true), 'https://twitter.com/search?src=typd&q=%23tag'); }); test('class URI cannot represent relative file paths #34449', function () { let path = '/foo/bar'; - assert.equal(URI.file(path).path, path); + assert.strictEqual(URI.file(path).path, path); path = 'foo/bar'; - assert.equal(URI.file(path).path, '/foo/bar'); + assert.strictEqual(URI.file(path).path, '/foo/bar'); path = './foo/bar'; - assert.equal(URI.file(path).path, '/./foo/bar'); // missing normalization + assert.strictEqual(URI.file(path).path, '/./foo/bar'); // missing normalization const fileUri1 = URI.parse(`file:foo/bar`); - assert.equal(fileUri1.path, '/foo/bar'); - assert.equal(fileUri1.authority, ''); + assert.strictEqual(fileUri1.path, '/foo/bar'); + assert.strictEqual(fileUri1.authority, ''); const uri = fileUri1.toString(); - assert.equal(uri, 'file:///foo/bar'); + assert.strictEqual(uri, 'file:///foo/bar'); const fileUri2 = URI.parse(uri); - assert.equal(fileUri2.path, '/foo/bar'); - assert.equal(fileUri2.authority, ''); + assert.strictEqual(fileUri2.path, '/foo/bar'); + assert.strictEqual(fileUri2.authority, ''); }); test('Ctrl click to follow hash query param url gets urlencoded #49628', function () { let input = 'http://localhost:3000/#/foo?bar=baz'; let uri = URI.parse(input); - assert.equal(uri.toString(true), input); + assert.strictEqual(uri.toString(true), input); input = 'http://localhost:3000/foo?bar=baz'; uri = URI.parse(input); - assert.equal(uri.toString(true), input); + assert.strictEqual(uri.toString(true), input); }); test('Unable to open \'%A0.txt\': URI malformed #76506', function () { let uri = URI.file('/foo/%A0.txt'); let uri2 = URI.parse(uri.toString()); - assert.equal(uri.scheme, uri2.scheme); - assert.equal(uri.path, uri2.path); + assert.strictEqual(uri.scheme, uri2.scheme); + assert.strictEqual(uri.path, uri2.path); uri = URI.file('/foo/%2e.txt'); uri2 = URI.parse(uri.toString()); - assert.equal(uri.scheme, uri2.scheme); - assert.equal(uri.path, uri2.path); + assert.strictEqual(uri.scheme, uri2.scheme); + assert.strictEqual(uri.path, uri2.path); + }); + + test('Bug in URI.isUri() that fails `thing` type comparison #114971', function () { + const uri = URI.file('/foo/bazz.txt'); + assert.strictEqual(URI.isUri(uri), true); + assert.strictEqual(URI.isUri(uri.toJSON()), false); + + // fsPath -> getter + assert.strictEqual(URI.isUri({ + scheme: 'file', + authority: '', + path: '/foo/bazz.txt', + get fsPath() { return '/foo/bazz.txt'; }, + query: '', + fragment: '', + with() { return this; }, + toString() { return ''; } + }), true); + + // fsPath -> property + assert.strictEqual(URI.isUri({ + scheme: 'file', + authority: '', + path: '/foo/bazz.txt', + fsPath: '/foo/bazz.txt', + query: '', + fragment: '', + with() { return this; }, + toString() { return ''; } + }), true); }); test('Unable to open \'%A0.txt\': URI malformed #76506', function () { - assert.equal(URI.parse('file://some/%.txt'), 'file://some/%25.txt'); - assert.equal(URI.parse('file://some/%A0.txt'), 'file://some/%25A0.txt'); + assert.strictEqual(URI.parse('file://some/%.txt').toString(), 'file://some/%25.txt'); + assert.strictEqual(URI.parse('file://some/%A0.txt').toString(), 'file://some/%25A0.txt'); }); - test('Links in markdown are broken if url contains encoded parameters #79474', function () { - this.skip(); + test.skip('Links in markdown are broken if url contains encoded parameters #79474', function () { let strIn = 'https://myhost.com/Redirect?url=http%3A%2F%2Fwww.bing.com%3Fsearch%3Dtom'; let uri1 = URI.parse(strIn); let strOut = uri1.toString(); let uri2 = URI.parse(strOut); - assert.equal(uri1.scheme, uri2.scheme); - assert.equal(uri1.authority, uri2.authority); - assert.equal(uri1.path, uri2.path); - assert.equal(uri1.query, uri2.query); - assert.equal(uri1.fragment, uri2.fragment); - assert.equal(strIn, strOut); // fails here!! + assert.strictEqual(uri1.scheme, uri2.scheme); + assert.strictEqual(uri1.authority, uri2.authority); + assert.strictEqual(uri1.path, uri2.path); + assert.strictEqual(uri1.query, uri2.query); + assert.strictEqual(uri1.fragment, uri2.fragment); + assert.strictEqual(strIn, strOut); // fails here!! }); - test('Uri#parse can break path-component #45515', function () { - this.skip(); + test.skip('Uri#parse can break path-component #45515', function () { let strIn = 'https://firebasestorage.googleapis.com/v0/b/brewlangerie.appspot.com/o/products%2FzVNZkudXJyq8bPGTXUxx%2FBetterave-Sesame.jpg?alt=media&token=0b2310c4-3ea6-4207-bbde-9c3710ba0437'; let uri1 = URI.parse(strIn); let strOut = uri1.toString(); let uri2 = URI.parse(strOut); - assert.equal(uri1.scheme, uri2.scheme); - assert.equal(uri1.authority, uri2.authority); - assert.equal(uri1.path, uri2.path); - assert.equal(uri1.query, uri2.query); - assert.equal(uri1.fragment, uri2.fragment); - assert.equal(strIn, strOut); // fails here!! + assert.strictEqual(uri1.scheme, uri2.scheme); + assert.strictEqual(uri1.authority, uri2.authority); + assert.strictEqual(uri1.path, uri2.path); + assert.strictEqual(uri1.query, uri2.query); + assert.strictEqual(uri1.fragment, uri2.fragment); + assert.strictEqual(strIn, strOut); // fails here!! }); test('URI - (de)serialize', function () { @@ -492,13 +520,13 @@ suite('URI', () => { let data = value.toJSON() as UriComponents; let clone = URI.revive(data); - assert.equal(clone.scheme, value.scheme); - assert.equal(clone.authority, value.authority); - assert.equal(clone.path, value.path); - assert.equal(clone.query, value.query); - assert.equal(clone.fragment, value.fragment); - assert.equal(clone.fsPath, value.fsPath); - assert.equal(clone.toString(), value.toString()); + assert.strictEqual(clone.scheme, value.scheme); + assert.strictEqual(clone.authority, value.authority); + assert.strictEqual(clone.path, value.path); + assert.strictEqual(clone.query, value.query); + assert.strictEqual(clone.fragment, value.fragment); + assert.strictEqual(clone.fsPath, value.fsPath); + assert.strictEqual(clone.toString(), value.toString()); } // } // console.profileEnd(); @@ -507,11 +535,11 @@ suite('URI', () => { const baseUri = URI.parse(base); const newUri = URI.joinPath(baseUri, fragment); const actual = newUri.toString(true); - assert.equal(actual, expected); + assert.strictEqual(actual, expected); if (checkWithUrl) { const actualUrl = new URL(fragment, base).href; - assert.equal(actualUrl, expected, 'DIFFERENT from URL'); + assert.strictEqual(actualUrl, expected, 'DIFFERENT from URL'); } } test('URI#joinPath', function () { diff --git a/src/vs/base/test/common/utils.ts b/src/vs/base/test/common/utils.ts index a5ebd13e..63b0541b 100644 --- a/src/vs/base/test/common/utils.ts +++ b/src/vs/base/test/common/utils.ts @@ -5,47 +5,10 @@ import { join } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; -import { canceled } from 'vs/base/common/errors'; import { isWindows } from 'vs/base/common/platform'; export type ValueCallback = (value: T | Promise) => void; -export class DeferredPromise { - - private completeCallback!: ValueCallback; - private errorCallback!: (err: any) => void; - - public p: Promise; - - constructor() { - this.p = new Promise((c, e) => { - this.completeCallback = c; - this.errorCallback = e; - }); - } - - public complete(value: T) { - return new Promise(resolve => { - this.completeCallback(value); - resolve(); - }); - } - - public error(err: any) { - return new Promise(resolve => { - this.errorCallback(err); - resolve(); - }); - } - - public cancel() { - new Promise(resolve => { - this.errorCallback(canceled()); - resolve(); - }); - } -} - export function toResource(this: any, path: string) { if (isWindows) { return URI.file(join('C:\\', btoa(this.test.fullTitle()), path)); @@ -60,7 +23,7 @@ export function suiteRepeat(n: number, description: string, callback: (this: any } } -export function testRepeat(n: number, description: string, callback: (this: any, done: MochaDone) => any): void { +export function testRepeat(n: number, description: string, callback: (this: any) => any): void { for (let i = 0; i < n; i++) { test(`${description} (iteration ${i})`, callback); } diff --git a/src/vs/base/test/common/uuid.test.ts b/src/vs/base/test/common/uuid.test.ts index ce07ab9c..632366bc 100644 --- a/src/vs/base/test/common/uuid.test.ts +++ b/src/vs/base/test/common/uuid.test.ts @@ -8,8 +8,8 @@ import * as uuid from 'vs/base/common/uuid'; suite('UUID', () => { test('generation', () => { const asHex = uuid.generateUuid(); - assert.equal(asHex.length, 36); - assert.equal(asHex[14], '4'); + assert.strictEqual(asHex.length, 36); + assert.strictEqual(asHex[14], '4'); assert.ok(asHex[19] === '8' || asHex[19] === '9' || asHex[19] === 'a' || asHex[19] === 'b'); }); diff --git a/src/vs/base/test/node/crypto.test.ts b/src/vs/base/test/node/crypto.test.ts index ad8dc4fa..16cfc58f 100644 --- a/src/vs/base/test/node/crypto.test.ts +++ b/src/vs/base/test/node/crypto.test.ts @@ -4,24 +4,29 @@ *--------------------------------------------------------------------------------------------*/ import { checksum } from 'vs/base/node/crypto'; -import { generateUuid } from 'vs/base/common/uuid'; import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { mkdirp, rimraf, RimRafMode, writeFile } from 'vs/base/node/pfs'; +import { mkdirp, rimraf, writeFile } from 'vs/base/node/pfs'; +import { getRandomTestPath } from 'vs/base/test/node/testUtils'; suite('Crypto', () => { + let testDir: string; + + setup(function () { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'crypto'); + + return mkdirp(testDir); + }); + + teardown(function () { + return rimraf(testDir); + }); + test('checksum', async () => { - const id = generateUuid(); - const testDir = join(tmpdir(), 'vsctests', id); const testFile = join(testDir, 'checksum.txt'); - - await mkdirp(testDir); - await writeFile(testFile, 'Hello World'); await checksum(testFile, '0a4d55a8d778e5022fab701977c5d840bbc486d0'); - - await rimraf(testDir, RimRafMode.MOVE); }); }); diff --git a/src/vs/base/test/node/decoder.test.ts b/src/vs/base/test/node/decoder.test.ts index f4d34d02..aa2e867c 100644 --- a/src/vs/base/test/node/decoder.test.ts +++ b/src/vs/base/test/node/decoder.test.ts @@ -4,19 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as decoder from 'vs/base/node/decoder'; +import { LineDecoder } from 'vs/base/node/decoder'; suite('Decoder', () => { test('decoding', () => { - const lineDecoder = new decoder.LineDecoder(); + const lineDecoder = new LineDecoder(); let res = lineDecoder.write(Buffer.from('hello')); - assert.equal(res.length, 0); + assert.strictEqual(res.length, 0); res = lineDecoder.write(Buffer.from('\nworld')); - assert.equal(res[0], 'hello'); - assert.equal(res.length, 1); + assert.strictEqual(res[0], 'hello'); + assert.strictEqual(res.length, 1); - assert.equal(lineDecoder.end(), 'world'); + assert.strictEqual(lineDecoder.end(), 'world'); }); -}); \ No newline at end of file +}); diff --git a/src/vs/base/test/node/extpath.test.ts b/src/vs/base/test/node/extpath.test.ts index e435d624..056110be 100644 --- a/src/vs/base/test/node/extpath.test.ts +++ b/src/vs/base/test/node/extpath.test.ts @@ -4,70 +4,52 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as os from 'os'; -import * as path from 'vs/base/common/path'; -import * as uuid from 'vs/base/common/uuid'; -import * as pfs from 'vs/base/node/pfs'; +import { tmpdir } from 'os'; +import { mkdirp, rimraf } from 'vs/base/node/pfs'; import { realcaseSync, realpath, realpathSync } from 'vs/base/node/extpath'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; -suite('Extpath', () => { +flakySuite('Extpath', () => { + let testDir: string; + + setup(() => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'extpath'); + + return mkdirp(testDir, 493); + }); + + teardown(() => { + return rimraf(testDir); + }); test('realcase', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extpath', id); - - await pfs.mkdirp(newDir, 493); // assume case insensitive file system if (process.platform === 'win32' || process.platform === 'darwin') { - const upper = newDir.toUpperCase(); + const upper = testDir.toUpperCase(); const real = realcaseSync(upper); if (real) { // can be null in case of permission errors - assert.notEqual(real, upper); - assert.equal(real.toUpperCase(), upper); - assert.equal(real, newDir); + assert.notStrictEqual(real, upper); + assert.strictEqual(real.toUpperCase(), upper); + assert.strictEqual(real, testDir); } } // linux, unix, etc. -> assume case sensitive file system else { - const real = realcaseSync(newDir); - assert.equal(real, newDir); + const real = realcaseSync(testDir); + assert.strictEqual(real, testDir); } - - await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); }); test('realpath', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extpath', id); - - await pfs.mkdirp(newDir, 493); - - const realpathVal = await realpath(newDir); + const realpathVal = await realpath(testDir); assert.ok(realpathVal); - - await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); }); - test('realpathSync', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extpath', id); - - await pfs.mkdirp(newDir, 493); - - let realpath!: string; - try { - realpath = realpathSync(newDir); - } catch (error) { - assert.ok(!error); - } - assert.ok(realpath!); - - await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + test('realpathSync', () => { + const realpath = realpathSync(testDir); + assert.ok(realpath); }); }); diff --git a/src/vs/base/test/node/id.test.ts b/src/vs/base/test/node/id.test.ts index 637afa5b..4d124163 100644 --- a/src/vs/base/test/node/id.test.ts +++ b/src/vs/base/test/node/id.test.ts @@ -2,22 +2,21 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; import { getMachineId } from 'vs/base/node/id'; import { getMac } from 'vs/base/node/macAddress'; +import { flakySuite } from 'vs/base/test/node/testUtils'; -suite('ID', () => { +flakySuite('ID', () => { - test('getMachineId', function () { - this.timeout(20000); - return getMachineId().then(id => { - assert.ok(id); - }); + test('getMachineId', async function () { + const id = await getMachineId(); + assert.ok(id); }); - test('getMac', () => { - return getMac().then(macAddress => { - assert.ok(/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/.test(macAddress), `Expected a MAC address, got: ${macAddress}`); - }); + test('getMac', async () => { + const macAddress = await getMac(); + assert.ok(/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/.test(macAddress), `Expected a MAC address, got: ${macAddress}`); }); -}); \ No newline at end of file +}); diff --git a/src/vs/base/test/node/keytar.test.ts b/src/vs/base/test/node/keytar.test.ts index b2f7bab5..4e187264 100644 --- a/src/vs/base/test/node/keytar.test.ts +++ b/src/vs/base/test/node/keytar.test.ts @@ -2,36 +2,30 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; -import * as platform from 'vs/base/common/platform'; +import { isLinux } from 'vs/base/common/platform'; suite('Keytar', () => { - test('loads and is functional', function (done) { - if (platform.isLinux) { - // Skip test due to set up issue with Travis. - this.skip(); - return; - } - (async () => { - const keytar = await import('keytar'); - const name = `VSCode Test ${Math.floor(Math.random() * 1e9)}`; + (isLinux ? test.skip : test)('loads and is functional', async () => { // TODO@RMacfarlane test seems to fail on Linux (Error: Unknown or unsupported transport 'disabled' for address 'disabled:') + const keytar = await import('keytar'); + const name = `VSCode Test ${Math.floor(Math.random() * 1e9)}`; + try { + await keytar.setPassword(name, 'foo', 'bar'); + assert.strictEqual(await keytar.findPassword(name), 'bar'); + assert.strictEqual((await keytar.findCredentials(name)).length, 1); + assert.strictEqual(await keytar.getPassword(name, 'foo'), 'bar'); + await keytar.deletePassword(name, 'foo'); + assert.strictEqual(await keytar.getPassword(name, 'foo'), null); + } catch (err) { + // try to clean up try { - await keytar.setPassword(name, 'foo', 'bar'); - assert.equal(await keytar.findPassword(name), 'bar'); - assert.equal((await keytar.findCredentials(name)).length, 1); - assert.equal(await keytar.getPassword(name, 'foo'), 'bar'); await keytar.deletePassword(name, 'foo'); - assert.equal(await keytar.getPassword(name, 'foo'), undefined); - } catch (err) { - // try to clean up - try { - await keytar.deletePassword(name, 'foo'); - } finally { - // eslint-disable-next-line no-unsafe-finally - throw err; - } + } finally { + // eslint-disable-next-line no-unsafe-finally + throw err; } - })().then(done, done); + } }); }); diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts index fd324076..eeb380a6 100644 --- a/src/vs/base/test/node/pfs/pfs.test.ts +++ b/src/vs/base/test/node/pfs/pfs.test.ts @@ -4,388 +4,306 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as os from 'os'; -import * as path from 'vs/base/common/path'; import * as fs from 'fs'; -import * as uuid from 'vs/base/common/uuid'; -import * as pfs from 'vs/base/node/pfs'; +import { tmpdir } from 'os'; +import { join, sep } from 'vs/base/common/path'; +import { generateUuid } from 'vs/base/common/uuid'; +import { copy, exists, mkdirp, move, readdir, readDirsInDir, readdirWithFileTypes, readFile, renameIgnoreError, rimraf, RimRafMode, rimrafSync, statLink, writeFile, writeFileSync } from 'vs/base/node/pfs'; import { timeout } from 'vs/base/common/async'; import { getPathFromAmdModule } from 'vs/base/common/amd'; -import { isWindows } from 'vs/base/common/platform'; import { canNormalize } from 'vs/base/common/normalization'; import { VSBuffer } from 'vs/base/common/buffer'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; -suite('PFS', function () { +flakySuite('PFS', function () { - // Given issues such as https://github.com/microsoft/vscode/issues/84066 - // we see random test failures when accessing the native file system. To - // diagnose further, we retry node.js file access tests up to 3 times to - // rule out any random disk issue. - this.retries(3); + let testDir: string; + + setup(() => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'pfs'); + + return mkdirp(testDir, 493); + }); + + teardown(() => { + return rimraf(testDir); + }); test('writeFile', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'writefile.txt'); + const testFile = join(testDir, 'writefile.txt'); - await pfs.mkdirp(newDir, 493); - assert.ok(fs.existsSync(newDir)); + assert.ok(!(await exists(testFile))); - await pfs.writeFile(testFile, 'Hello World', (null!)); - assert.equal(fs.readFileSync(testFile), 'Hello World'); + await writeFile(testFile, 'Hello World', (null!)); - await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + assert.strictEqual((await readFile(testFile)).toString(), 'Hello World'); }); test('writeFile - parallel write on different files works', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile1 = path.join(newDir, 'writefile1.txt'); - const testFile2 = path.join(newDir, 'writefile2.txt'); - const testFile3 = path.join(newDir, 'writefile3.txt'); - const testFile4 = path.join(newDir, 'writefile4.txt'); - const testFile5 = path.join(newDir, 'writefile5.txt'); - - await pfs.mkdirp(newDir, 493); - assert.ok(fs.existsSync(newDir)); + const testFile1 = join(testDir, 'writefile1.txt'); + const testFile2 = join(testDir, 'writefile2.txt'); + const testFile3 = join(testDir, 'writefile3.txt'); + const testFile4 = join(testDir, 'writefile4.txt'); + const testFile5 = join(testDir, 'writefile5.txt'); await Promise.all([ - pfs.writeFile(testFile1, 'Hello World 1', (null!)), - pfs.writeFile(testFile2, 'Hello World 2', (null!)), - pfs.writeFile(testFile3, 'Hello World 3', (null!)), - pfs.writeFile(testFile4, 'Hello World 4', (null!)), - pfs.writeFile(testFile5, 'Hello World 5', (null!)) + writeFile(testFile1, 'Hello World 1', (null!)), + writeFile(testFile2, 'Hello World 2', (null!)), + writeFile(testFile3, 'Hello World 3', (null!)), + writeFile(testFile4, 'Hello World 4', (null!)), + writeFile(testFile5, 'Hello World 5', (null!)) ]); - assert.equal(fs.readFileSync(testFile1), 'Hello World 1'); - assert.equal(fs.readFileSync(testFile2), 'Hello World 2'); - assert.equal(fs.readFileSync(testFile3), 'Hello World 3'); - assert.equal(fs.readFileSync(testFile4), 'Hello World 4'); - assert.equal(fs.readFileSync(testFile5), 'Hello World 5'); - - await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + assert.strictEqual(fs.readFileSync(testFile1).toString(), 'Hello World 1'); + assert.strictEqual(fs.readFileSync(testFile2).toString(), 'Hello World 2'); + assert.strictEqual(fs.readFileSync(testFile3).toString(), 'Hello World 3'); + assert.strictEqual(fs.readFileSync(testFile4).toString(), 'Hello World 4'); + assert.strictEqual(fs.readFileSync(testFile5).toString(), 'Hello World 5'); }); test('writeFile - parallel write on same files works and is sequentalized', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'writefile.txt'); - - await pfs.mkdirp(newDir, 493); - assert.ok(fs.existsSync(newDir)); + const testFile = join(testDir, 'writefile.txt'); await Promise.all([ - pfs.writeFile(testFile, 'Hello World 1', undefined), - pfs.writeFile(testFile, 'Hello World 2', undefined), - timeout(10).then(() => pfs.writeFile(testFile, 'Hello World 3', undefined)), - pfs.writeFile(testFile, 'Hello World 4', undefined), - timeout(10).then(() => pfs.writeFile(testFile, 'Hello World 5', undefined)) + writeFile(testFile, 'Hello World 1', undefined), + writeFile(testFile, 'Hello World 2', undefined), + timeout(10).then(() => writeFile(testFile, 'Hello World 3', undefined)), + writeFile(testFile, 'Hello World 4', undefined), + timeout(10).then(() => writeFile(testFile, 'Hello World 5', undefined)) ]); - assert.equal(fs.readFileSync(testFile), 'Hello World 5'); - - await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + assert.strictEqual(fs.readFileSync(testFile).toString(), 'Hello World 5'); }); test('rimraf - simple - unlink', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - await pfs.rimraf(newDir); - assert.ok(!fs.existsSync(newDir)); + await rimraf(testDir); + assert.ok(!fs.existsSync(testDir)); }); test('rimraf - simple - move', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - await pfs.rimraf(newDir, pfs.RimRafMode.MOVE); - assert.ok(!fs.existsSync(newDir)); + await rimraf(testDir, RimRafMode.MOVE); + assert.ok(!fs.existsSync(testDir)); }); test('rimraf - recursive folder structure - unlink', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); + fs.mkdirSync(join(testDir, 'somefolder')); + fs.writeFileSync(join(testDir, 'somefolder', 'somefile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - fs.mkdirSync(path.join(newDir, 'somefolder')); - fs.writeFileSync(path.join(newDir, 'somefolder', 'somefile.txt'), 'Contents'); - - await pfs.rimraf(newDir); - assert.ok(!fs.existsSync(newDir)); + await rimraf(testDir); + assert.ok(!fs.existsSync(testDir)); }); test('rimraf - recursive folder structure - move', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); + fs.mkdirSync(join(testDir, 'somefolder')); + fs.writeFileSync(join(testDir, 'somefolder', 'somefile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - fs.mkdirSync(path.join(newDir, 'somefolder')); - fs.writeFileSync(path.join(newDir, 'somefolder', 'somefile.txt'), 'Contents'); - - await pfs.rimraf(newDir, pfs.RimRafMode.MOVE); - assert.ok(!fs.existsSync(newDir)); + await rimraf(testDir, RimRafMode.MOVE); + assert.ok(!fs.existsSync(testDir)); }); test('rimraf - simple ends with dot - move', async () => { - const id = `${uuid.generateUuid()}.`; - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - await pfs.rimraf(newDir, pfs.RimRafMode.MOVE); - assert.ok(!fs.existsSync(newDir)); + await rimraf(testDir, RimRafMode.MOVE); + assert.ok(!fs.existsSync(testDir)); }); test('rimraf - simple ends with dot slash/backslash - move', async () => { - const id = `${uuid.generateUuid()}.`; - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - await pfs.rimraf(`${newDir}${path.sep}`, pfs.RimRafMode.MOVE); - assert.ok(!fs.existsSync(newDir)); + await rimraf(`${testDir}${sep}`, RimRafMode.MOVE); + assert.ok(!fs.existsSync(testDir)); }); test('rimrafSync - swallows file not found error', function () { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + const nonExistingDir = join(testDir, 'not-existing'); + rimrafSync(nonExistingDir); - pfs.rimrafSync(newDir); - - assert.ok(!fs.existsSync(newDir)); + assert.ok(!fs.existsSync(nonExistingDir)); }); test('rimrafSync - simple', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); + rimrafSync(testDir); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - pfs.rimrafSync(newDir); - - assert.ok(!fs.existsSync(newDir)); + assert.ok(!fs.existsSync(testDir)); }); test('rimrafSync - recursive folder structure', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); + fs.mkdirSync(join(testDir, 'somefolder')); + fs.writeFileSync(join(testDir, 'somefolder', 'somefile.txt'), 'Contents'); - fs.mkdirSync(path.join(newDir, 'somefolder')); - fs.writeFileSync(path.join(newDir, 'somefolder', 'somefile.txt'), 'Contents'); + rimrafSync(testDir); - pfs.rimrafSync(newDir); - - assert.ok(!fs.existsSync(newDir)); + assert.ok(!fs.existsSync(testDir)); }); - test('moveIgnoreError', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - - await pfs.mkdirp(newDir, 493); - try { - await pfs.renameIgnoreError(path.join(newDir, 'foo'), path.join(newDir, 'bar')); - return pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); - } - catch (error) { - assert.fail(error); - } + test('moveIgnoreError', () => { + return renameIgnoreError(join(testDir, 'foo'), join(testDir, 'bar')); }); test('copy, move and delete', async () => { - const id = uuid.generateUuid(); - const id2 = uuid.generateUuid(); + const id = generateUuid(); + const id2 = generateUuid(); const sourceDir = getPathFromAmdModule(require, './fixtures'); - const parentDir = path.join(os.tmpdir(), 'vsctests', 'pfs'); - const targetDir = path.join(parentDir, id); - const targetDir2 = path.join(parentDir, id2); + const parentDir = join(tmpdir(), 'vsctests', 'pfs'); + const targetDir = join(parentDir, id); + const targetDir2 = join(parentDir, id2); - await pfs.copy(sourceDir, targetDir); + await copy(sourceDir, targetDir); assert.ok(fs.existsSync(targetDir)); - assert.ok(fs.existsSync(path.join(targetDir, 'index.html'))); - assert.ok(fs.existsSync(path.join(targetDir, 'site.css'))); - assert.ok(fs.existsSync(path.join(targetDir, 'examples'))); - assert.ok(fs.statSync(path.join(targetDir, 'examples')).isDirectory()); - assert.ok(fs.existsSync(path.join(targetDir, 'examples', 'small.jxs'))); + assert.ok(fs.existsSync(join(targetDir, 'index.html'))); + assert.ok(fs.existsSync(join(targetDir, 'site.css'))); + assert.ok(fs.existsSync(join(targetDir, 'examples'))); + assert.ok(fs.statSync(join(targetDir, 'examples')).isDirectory()); + assert.ok(fs.existsSync(join(targetDir, 'examples', 'small.jxs'))); - await pfs.move(targetDir, targetDir2); + await move(targetDir, targetDir2); assert.ok(!fs.existsSync(targetDir)); assert.ok(fs.existsSync(targetDir2)); - assert.ok(fs.existsSync(path.join(targetDir2, 'index.html'))); - assert.ok(fs.existsSync(path.join(targetDir2, 'site.css'))); - assert.ok(fs.existsSync(path.join(targetDir2, 'examples'))); - assert.ok(fs.statSync(path.join(targetDir2, 'examples')).isDirectory()); - assert.ok(fs.existsSync(path.join(targetDir2, 'examples', 'small.jxs'))); + assert.ok(fs.existsSync(join(targetDir2, 'index.html'))); + assert.ok(fs.existsSync(join(targetDir2, 'site.css'))); + assert.ok(fs.existsSync(join(targetDir2, 'examples'))); + assert.ok(fs.statSync(join(targetDir2, 'examples')).isDirectory()); + assert.ok(fs.existsSync(join(targetDir2, 'examples', 'small.jxs'))); - await pfs.move(path.join(targetDir2, 'index.html'), path.join(targetDir2, 'index_moved.html')); + await move(join(targetDir2, 'index.html'), join(targetDir2, 'index_moved.html')); - assert.ok(!fs.existsSync(path.join(targetDir2, 'index.html'))); - assert.ok(fs.existsSync(path.join(targetDir2, 'index_moved.html'))); + assert.ok(!fs.existsSync(join(targetDir2, 'index.html'))); + assert.ok(fs.existsSync(join(targetDir2, 'index_moved.html'))); - await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + await rimraf(parentDir); assert.ok(!fs.existsSync(parentDir)); }); - test('mkdirp', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + test('copy skips over dangling symbolic links', async () => { + const id1 = generateUuid(); + const symbolicLinkTarget = join(testDir, id1); - await pfs.mkdirp(newDir, 493); + const id2 = generateUuid(); + const symbolicLink = join(testDir, id2); + + const id3 = generateUuid(); + const copyTarget = join(testDir, id3); + + await mkdirp(symbolicLinkTarget, 493); + + fs.symlinkSync(symbolicLinkTarget, symbolicLink, 'junction'); + + await rimraf(symbolicLinkTarget); + + await copy(symbolicLink, copyTarget); // this should not throw + + assert.ok(!fs.existsSync(copyTarget)); + }); + + test('mkdirp', async () => { + const newDir = join(testDir, generateUuid()); + + await mkdirp(newDir, 493); assert.ok(fs.existsSync(newDir)); - - return pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); }); test('readDirsInDir', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.mkdirSync(join(testDir, 'somefolder1')); + fs.mkdirSync(join(testDir, 'somefolder2')); + fs.mkdirSync(join(testDir, 'somefolder3')); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - - fs.mkdirSync(path.join(newDir, 'somefolder1')); - fs.mkdirSync(path.join(newDir, 'somefolder2')); - fs.mkdirSync(path.join(newDir, 'somefolder3')); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - const result = await pfs.readDirsInDir(newDir); - assert.equal(result.length, 3); + const result = await readDirsInDir(testDir); + assert.strictEqual(result.length, 3); assert.ok(result.indexOf('somefolder1') !== -1); assert.ok(result.indexOf('somefolder2') !== -1); assert.ok(result.indexOf('somefolder3') !== -1); - - await pfs.rimraf(newDir); }); test('stat link', async () => { - if (isWindows) { - return; // Symlinks are not the same on win, and we can not create them programitically without admin privileges - } + const id1 = generateUuid(); + const directory = join(testDir, id1); - const id1 = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id1); - const directory = path.join(parentDir, 'pfs', id1); + const id2 = generateUuid(); + const symbolicLink = join(testDir, id2); - const id2 = uuid.generateUuid(); - const symbolicLink = path.join(parentDir, 'pfs', id2); + await mkdirp(directory, 493); - await pfs.mkdirp(directory, 493); + fs.symlinkSync(directory, symbolicLink, 'junction'); - fs.symlinkSync(directory, symbolicLink); - - let statAndIsLink = await pfs.statLink(directory); + let statAndIsLink = await statLink(directory); assert.ok(!statAndIsLink?.symbolicLink); - statAndIsLink = await pfs.statLink(symbolicLink); + statAndIsLink = await statLink(symbolicLink); assert.ok(statAndIsLink?.symbolicLink); assert.ok(!statAndIsLink?.symbolicLink?.dangling); - - pfs.rimrafSync(directory); }); test('stat link (non existing target)', async () => { - if (isWindows) { - return; // Symlinks are not the same on win, and we can not create them programitically without admin privileges - } + const id1 = generateUuid(); + const directory = join(testDir, id1); - const id1 = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id1); - const directory = path.join(parentDir, 'pfs', id1); + const id2 = generateUuid(); + const symbolicLink = join(testDir, id2); - const id2 = uuid.generateUuid(); - const symbolicLink = path.join(parentDir, 'pfs', id2); + await mkdirp(directory, 493); - await pfs.mkdirp(directory, 493); + fs.symlinkSync(directory, symbolicLink, 'junction'); - fs.symlinkSync(directory, symbolicLink); + await rimraf(directory); - pfs.rimrafSync(directory); - - const statAndIsLink = await pfs.statLink(symbolicLink); + const statAndIsLink = await statLink(symbolicLink); assert.ok(statAndIsLink?.symbolicLink); assert.ok(statAndIsLink?.symbolicLink?.dangling); }); test('readdir', async () => { if (canNormalize && typeof process.versions['electron'] !== 'undefined' /* needs electron */) { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id, 'öäü'); + const id = generateUuid(); + const newDir = join(testDir, 'pfs', id, 'öäü'); - await pfs.mkdirp(newDir, 493); + await mkdirp(newDir, 493); assert.ok(fs.existsSync(newDir)); - const children = await pfs.readdir(path.join(parentDir, 'pfs', id)); - assert.equal(children.some(n => n === 'öäü'), true); // Mac always converts to NFD, so - - await pfs.rimraf(parentDir); + const children = await readdir(join(testDir, 'pfs', id)); + assert.strictEqual(children.some(n => n === 'öäü'), true); // Mac always converts to NFD, so } }); test('readdirWithFileTypes', async () => { if (canNormalize && typeof process.versions['electron'] !== 'undefined' /* needs electron */) { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const testDir = path.join(parentDir, 'pfs', id); + const newDir = join(testDir, 'öäü'); + await mkdirp(newDir, 493); - const newDir = path.join(testDir, 'öäü'); - await pfs.mkdirp(newDir, 493); - - await pfs.writeFile(path.join(testDir, 'somefile.txt'), 'contents'); + await writeFile(join(testDir, 'somefile.txt'), 'contents'); assert.ok(fs.existsSync(newDir)); - const children = await pfs.readdirWithFileTypes(testDir); + const children = await readdirWithFileTypes(testDir); - assert.equal(children.some(n => n.name === 'öäü'), true); // Mac always converts to NFD, so - assert.equal(children.some(n => n.isDirectory()), true); + assert.strictEqual(children.some(n => n.name === 'öäü'), true); // Mac always converts to NFD, so + assert.strictEqual(children.some(n => n.isDirectory()), true); - assert.equal(children.some(n => n.name === 'somefile.txt'), true); - assert.equal(children.some(n => n.isFile()), true); - - await pfs.rimraf(parentDir); + assert.strictEqual(children.some(n => n.name === 'somefile.txt'), true); + assert.strictEqual(children.some(n => n.isFile()), true); } }); @@ -416,65 +334,41 @@ suite('PFS', function () { bigData: string | Buffer | Uint8Array, bigDataValue: string ): Promise { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'flushed.txt'); + const testFile = join(testDir, 'flushed.txt'); - await pfs.mkdirp(newDir, 493); - assert.ok(fs.existsSync(newDir)); + assert.ok(fs.existsSync(testDir)); - await pfs.writeFile(testFile, smallData); - assert.equal(fs.readFileSync(testFile), smallDataValue); + await writeFile(testFile, smallData); + assert.strictEqual(fs.readFileSync(testFile).toString(), smallDataValue); - await pfs.writeFile(testFile, bigData); - assert.equal(fs.readFileSync(testFile), bigDataValue); - - await pfs.rimraf(parentDir); + await writeFile(testFile, bigData); + assert.strictEqual(fs.readFileSync(testFile).toString(), bigDataValue); } test('writeFile (string, error handling)', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'flushed.txt'); + const testFile = join(testDir, 'flushed.txt'); - await pfs.mkdirp(newDir, 493); - - assert.ok(fs.existsSync(newDir)); - - fs.mkdirSync(testFile); // this will trigger an error because testFile is now a directory! + fs.mkdirSync(testFile); // this will trigger an error later because testFile is now a directory! let expectedError: Error | undefined; try { - await pfs.writeFile(testFile, 'Hello World'); + await writeFile(testFile, 'Hello World'); } catch (error) { expectedError = error; } assert.ok(expectedError); - - await pfs.rimraf(parentDir); }); test('writeFileSync', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'flushed.txt'); + const testFile = join(testDir, 'flushed.txt'); - await pfs.mkdirp(newDir, 493); - - assert.ok(fs.existsSync(newDir)); - - pfs.writeFileSync(testFile, 'Hello World'); - assert.equal(fs.readFileSync(testFile), 'Hello World'); + writeFileSync(testFile, 'Hello World'); + assert.strictEqual(fs.readFileSync(testFile).toString(), 'Hello World'); const largeString = (new Array(100 * 1024)).join('Large String\n'); - pfs.writeFileSync(testFile, largeString); - assert.equal(fs.readFileSync(testFile), largeString); - - await pfs.rimraf(parentDir); + writeFileSync(testFile, largeString); + assert.strictEqual(fs.readFileSync(testFile).toString(), largeString); }); }); diff --git a/src/vs/base/test/node/port.test.ts b/src/vs/base/test/node/port.test.ts index 87d8eca9..12004465 100644 --- a/src/vs/base/test/node/port.test.ts +++ b/src/vs/base/test/node/port.test.ts @@ -6,14 +6,10 @@ import * as assert from 'assert'; import * as net from 'net'; import * as ports from 'vs/base/node/ports'; +import { flakySuite } from 'vs/base/test/node/testUtils'; -suite('Ports', () => { - test('Finds a free port (no timeout)', function (done) { - this.timeout(1000 * 10); // higher timeout for this test - - if (process.env['VSCODE_PID']) { - return done(); // this test fails when run from within VS Code - } +flakySuite('Ports', () => { + (process.env['VSCODE_PID'] ? test.skip /* this test fails when run from within VS Code */ : test)('Finds a free port (no timeout)', function (done) { // get an initial freeport >= 7000 ports.findFreePort(7000, 100, 300000).then(initialPort => { diff --git a/src/vs/base/test/node/powershell.test.ts b/src/vs/base/test/node/powershell.test.ts new file mode 100644 index 00000000..6e88b098 --- /dev/null +++ b/src/vs/base/test/node/powershell.test.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as platform from 'vs/base/common/platform'; +import { enumeratePowerShellInstallations, getFirstAvailablePowerShellInstallation, IPowerShellExeDetails } from 'vs/base/node/powershell'; + +function checkPath(exePath: string) { + // Check to see if the path exists + let pathCheckResult = false; + try { + const stat = fs.statSync(exePath); + pathCheckResult = stat.isFile(); + } catch { + // fs.exists throws on Windows with SymbolicLinks so we + // also use lstat to try and see if the file exists. + try { + pathCheckResult = fs.statSync(fs.readlinkSync(exePath)).isFile(); + } catch { + + } + } + + assert.strictEqual(pathCheckResult, true); +} + +if (platform.isWindows) { + suite('PowerShell finder', () => { + + test('Can find first available PowerShell', async () => { + const pwshExe = await getFirstAvailablePowerShellInstallation(); + const exePath = pwshExe?.exePath; + assert.notStrictEqual(exePath, null); + assert.notStrictEqual(pwshExe?.displayName, null); + + checkPath(exePath!); + }); + + test('Can enumerate PowerShells', async () => { + const isOS64Bit = os.arch() === 'x64'; + const pwshs = new Array(); + for await (const p of enumeratePowerShellInstallations()) { + pwshs.push(p); + } + + const powershellLog = 'Found these PowerShells:\n' + pwshs.map(p => `${p.displayName}: ${p.exePath}`).join('\n'); + assert.strictEqual(pwshs.length >= (isOS64Bit ? 2 : 1), true, powershellLog); + + for (const pwsh of pwshs) { + checkPath(pwsh.exePath); + } + + + const lastIndex = pwshs.length - 1; + const secondToLastIndex = pwshs.length - 2; + + // 64bit process on 64bit OS + if (process.arch === 'x64') { + checkPath(pwshs[secondToLastIndex].exePath); + assert.strictEqual(pwshs[secondToLastIndex].displayName, 'Windows PowerShell', powershellLog); + + checkPath(pwshs[lastIndex].exePath); + assert.strictEqual(pwshs[lastIndex].displayName, 'Windows PowerShell (x86)', powershellLog); + } else if (isOS64Bit) { + // 32bit process on 64bit OS + + // Windows PowerShell x86 comes first if vscode is 32bit + checkPath(pwshs[secondToLastIndex].exePath); + assert.strictEqual(pwshs[secondToLastIndex].displayName, 'Windows PowerShell (x86)', powershellLog); + + checkPath(pwshs[lastIndex].exePath); + assert.strictEqual(pwshs[lastIndex].displayName, 'Windows PowerShell', powershellLog); + } else { + // 32bit or ARM process + checkPath(pwshs[lastIndex].exePath); + assert.strictEqual(pwshs[lastIndex].displayName, 'Windows PowerShell (x86)', powershellLog); + } + }); + }); +} diff --git a/src/vs/base/test/node/processes/processes.test.ts b/src/vs/base/test/node/processes/processes.test.ts index 76719506..e9f922f3 100644 --- a/src/vs/base/test/node/processes/processes.test.ts +++ b/src/vs/base/test/node/processes/processes.test.ts @@ -13,9 +13,9 @@ import { getPathFromAmdModule } from 'vs/base/common/amd'; function fork(id: string): cp.ChildProcess { const opts: any = { env: objects.mixin(objects.deepClone(process.env), { - AMD_ENTRYPOINT: id, - PIPE_LOGGING: 'true', - VERBOSE_LOGGING: true + VSCODE_AMD_ENTRYPOINT: id, + VSCODE_PIPE_LOGGING: 'true', + VSCODE_VERBOSE_LOGGING: true }) }; @@ -59,11 +59,7 @@ suite('Processes', () => { }); }); - test('buffered sending - lots of data (potential deadlock on win32)', function (done: () => void) { - if (!platform.isWindows || process.env['VSCODE_PID']) { - return done(); // test is only relevant for Windows and seems to crash randomly on some Linux builds - } - + (!platform.isWindows || process.env['VSCODE_PID'] ? test.skip : test)('buffered sending - lots of data (potential deadlock on win32)', function (done: () => void) { // test is only relevant for Windows and seems to crash randomly on some Linux builds const child = fork('vs/base/test/node/processes/fixtures/fork_large'); const sender = processes.createQueuedSender(child); diff --git a/src/vs/base/test/node/testUtils.ts b/src/vs/base/test/node/testUtils.ts index 452e8ae0..802d9fd1 100644 --- a/src/vs/base/test/node/testUtils.ts +++ b/src/vs/base/test/node/testUtils.ts @@ -3,9 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type { Suite } from 'mocha'; import { join } from 'vs/base/common/path'; import { generateUuid } from 'vs/base/common/uuid'; export function getRandomTestPath(tmpdir: string, ...segments: string[]): string { return join(tmpdir, ...segments, generateUuid()); } + +export function flakySuite(title: string, fn: (this: Suite) => void): Suite { + return suite(title, function () { + + // Flaky suites need retries and timeout to complete + // e.g. because they access the file system which can + // be unreliable depending on the environment. + this.retries(3); + this.timeout(1000 * 20); + + // Invoke suite ensuring that `this` is + // properly wired in. + fn.call(this); + }); +} diff --git a/src/vs/base/test/node/zip/zip.test.ts b/src/vs/base/test/node/zip/zip.test.ts index f79bddaa..a98b2609 100644 --- a/src/vs/base/test/node/zip/zip.test.ts +++ b/src/vs/base/test/node/zip/zip.test.ts @@ -5,24 +5,33 @@ import * as assert from 'assert'; import * as path from 'vs/base/common/path'; -import * as os from 'os'; +import { tmpdir } from 'os'; import { extract } from 'vs/base/node/zip'; -import { generateUuid } from 'vs/base/common/uuid'; -import { rimraf, exists } from 'vs/base/node/pfs'; +import { rimraf, exists, mkdirp } from 'vs/base/node/pfs'; import { getPathFromAmdModule } from 'vs/base/common/amd'; import { createCancelablePromise } from 'vs/base/common/async'; - -const fixtures = getPathFromAmdModule(require, './fixtures'); +import { getRandomTestPath } from 'vs/base/test/node/testUtils'; suite('Zip', () => { - test('extract should handle directories', () => { - const fixture = path.join(fixtures, 'extract.zip'); - const target = path.join(os.tmpdir(), generateUuid()); + let testDir: string; - return createCancelablePromise(token => extract(fixture, target, {}, token) - .then(() => exists(path.join(target, 'extension'))) - .then(exists => assert(exists)) - .then(() => rimraf(target))); + setup(() => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'zip'); + + return mkdirp(testDir); + }); + + teardown(() => { + return rimraf(testDir); + }); + + test('extract should handle directories', async () => { + const fixtures = getPathFromAmdModule(require, './fixtures'); + const fixture = path.join(fixtures, 'extract.zip'); + + await createCancelablePromise(token => extract(fixture, testDir, {}, token)); + const doesExist = await exists(path.join(testDir, 'extension')); + assert(doesExist); }); }); diff --git a/src/vs/base/worker/defaultWorkerFactory.ts b/src/vs/base/worker/defaultWorkerFactory.ts index 51faabdf..21b3935f 100644 --- a/src/vs/base/worker/defaultWorkerFactory.ts +++ b/src/vs/base/worker/defaultWorkerFactory.ts @@ -6,6 +6,8 @@ import { globals } from 'vs/base/common/platform'; import { IWorker, IWorkerCallback, IWorkerFactory, logOnceWebWorkerWarning } from 'vs/base/common/worker/simpleWorker'; +const ttPolicy = window.trustedTypes?.createPolicy('defaultWorkerFactory', { createScriptURL: value => value }); + function getWorker(workerId: string, label: string): Worker | Promise { // Option for hosts to overwrite the worker script (used in the standalone editor) if (globals.MonacoEnvironment) { @@ -13,7 +15,8 @@ function getWorker(workerId: string, label: string): Worker | Promise { return globals.MonacoEnvironment.getWorker(workerId, label); } if (typeof globals.MonacoEnvironment.getWorkerUrl === 'function') { - return new Worker(globals.MonacoEnvironment.getWorkerUrl(workerId, label)); + const wokerUrl = globals.MonacoEnvironment.getWorkerUrl(workerId, label); + return new Worker(ttPolicy ? ttPolicy.createScriptURL(wokerUrl) as unknown as string : wokerUrl, { name: label }); } } // ESM-comment-begin @@ -21,7 +24,7 @@ function getWorker(workerId: string, label: string): Worker | Promise { // check if the JS lives on a different origin const workerMain = require.toUrl('./' + workerId); // explicitly using require.toUrl(), see https://github.com/microsoft/vscode/issues/107440#issuecomment-698982321 const workerUrl = getWorkerBootstrapUrl(workerMain, label); - return new Worker(workerUrl, { name: label }); + return new Worker(ttPolicy ? ttPolicy.createScriptURL(workerUrl) as unknown as string : workerUrl, { name: label }); } // ESM-comment-end throw new Error(`You must define a function MonacoEnvironment.getWorkerUrl or MonacoEnvironment.getWorker`); diff --git a/src/vs/code/browser/workbench/workbench-dev.html b/src/vs/code/browser/workbench/workbench-dev.html index 8b186929..2df6f812 100644 --- a/src/vs/code/browser/workbench/workbench-dev.html +++ b/src/vs/code/browser/workbench/workbench-dev.html @@ -3,8 +3,7 @@ @@ -56,7 +55,7 @@ @@ -55,7 +54,7 @@ diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index d1dba072..1ed7feec 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -265,7 +265,6 @@ class PollingURLCallbackProvider extends Disposable implements IURLCallbackProvi setTimeout(() => this.periodicFetchCallback(requestId, startTime), PollingURLCallbackProvider.FETCH_INTERVAL); } } - } class WorkspaceProvider implements IWorkspaceProvider { diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/deprecatedExtensionsCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/deprecatedExtensionsCleaner.ts new file mode 100644 index 00000000..0ffa0870 --- /dev/null +++ b/src/vs/code/electron-browser/sharedProcess/contrib/deprecatedExtensionsCleaner.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; + +export class DeprecatedExtensionsCleaner extends Disposable { + + constructor( + @IExtensionManagementService private readonly extensionManagementService: ExtensionManagementService + ) { + super(); + + this._register(extensionManagementService); // TODO@sandy081 this seems fishy + + this.cleanUpDeprecatedExtensions(); + } + + private cleanUpDeprecatedExtensions(): void { + this.extensionManagementService.removeDeprecatedExtensions(); + } +} diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater.ts b/src/vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater.ts new file mode 100644 index 00000000..6c9bfa81 --- /dev/null +++ b/src/vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; +import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; + +export class LocalizationsUpdater extends Disposable { + + constructor( + @ILocalizationsService private readonly localizationsService: LocalizationsService + ) { + super(); + + this.updateLocalizations(); + } + + private updateLocalizations(): void { + this.localizationsService.update(); + } +} diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcess.js b/src/vs/code/electron-browser/sharedProcess/sharedProcess.js index 8874e872..2bb0a0bb 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcess.js +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcess.js @@ -15,10 +15,7 @@ // Load shared process into window bootstrapWindow.load(['vs/code/electron-browser/sharedProcess/sharedProcessMain'], function (sharedProcess, configuration) { - sharedProcess.startup({ - machineId: configuration.machineId, - windowId: configuration.windowId - }); + return sharedProcess.main(configuration); }); diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 3dcc360b..3aa7cce1 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -3,15 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as platform from 'vs/base/common/platform'; import product from 'vs/platform/product/common/product'; -import { serve, Server, connect } from 'vs/base/parts/ipc/node/ipc.net'; +import * as fs from 'fs'; +import { release } from 'os'; +import { gracefulify } from 'graceful-fs'; +import { Server as MessagePortServer } from 'vs/base/parts/ipc/electron-sandbox/ipc.mp'; +import { StaticRouter, createChannelSender, createChannelReceiver } from 'vs/base/parts/ipc/common/ipc'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; import { ExtensionManagementChannel, ExtensionTipsChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService, IExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -23,24 +24,23 @@ import { IRequestService } from 'vs/platform/request/common/request'; import { RequestService } from 'vs/platform/request/browser/requestService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { combinedAppender, NullTelemetryService, ITelemetryAppender, NullAppender } from 'vs/platform/telemetry/common/telemetryUtils'; -import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; -import { TelemetryAppenderChannel } from 'vs/platform/telemetry/node/telemetryIpc'; -import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; +import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties'; +import { TelemetryAppenderChannel } from 'vs/platform/telemetry/common/telemetryIpc'; +import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; -import { ILogService, LogLevel, ILoggerService } from 'vs/platform/log/common/log'; +import { ILogService, ILoggerService, MultiplexLogService, ConsoleLogService } from 'vs/platform/log/common/log'; import { LoggerChannelClient, FollowerLogService } from 'vs/platform/log/common/logIpc'; import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; -import { combinedDisposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { combinedDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { DownloadService } from 'vs/platform/download/common/downloadService'; import { IDownloadService } from 'vs/platform/download/common/download'; -import { IChannel, IServerChannel, StaticRouter, createChannelSender, createChannelReceiver } from 'vs/base/parts/ipc/common/ipc'; import { NodeCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner'; import { LanguagePackCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner'; import { StorageDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner'; import { LogsDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner'; -import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; +import { IMainProcessService, MessagePortMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { SpdLogService } from 'vs/platform/log/node/spdlogService'; import { DiagnosticsService, IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; import { FileService } from 'vs/platform/files/common/fileService'; @@ -48,7 +48,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, IUserDataSyncStoreManagementService, IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration as registerUserDataSyncConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, IUserDataSyncStoreManagementService, IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { UserDataSyncStoreService, UserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { UserDataSyncChannel, UserDataSyncUtilServiceClient, UserDataAutoSyncChannel, UserDataSyncMachinesServiceChannel, UserDataSyncAccountServiceChannel, UserDataSyncStoreManagementServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; @@ -67,146 +67,184 @@ import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-s import { UserDataSyncMachinesService, IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { ExtensionRecommendationNotificationServiceChannelClient } from 'vs/platform/extensionRecommendations/electron-sandbox/extensionRecommendationsIpc'; -import { ActiveWindowManager } from 'vs/platform/windows/common/windowTracker'; +import { ActiveWindowManager } from 'vs/platform/windows/node/windowTracker'; import { TelemetryLogAppender } from 'vs/platform/telemetry/common/telemetryLogAppender'; import { UserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; import { IgnoredExtensionsManagementService, IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; import { ExtensionsStorageSyncService, IExtensionsStorageSyncService } from 'vs/platform/userDataSync/common/extensionsStorageSync'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ISharedProcessConfiguration } from 'vs/platform/sharedProcess/node/sharedProcess'; +import { LocalizationsUpdater } from 'vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater'; +import { DeprecatedExtensionsCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/deprecatedExtensionsCleaner'; +import { onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; -export interface ISharedProcessConfiguration { - readonly machineId: string; - readonly windowId: number; -} +class SharedProcessMain extends Disposable { -export function startup(configuration: ISharedProcessConfiguration) { - handshake(configuration); -} + private server = this._register(new MessagePortServer()); -interface ISharedProcessInitData { - sharedIPCHandle: string; - args: NativeParsedArgs; - logLevel: LogLevel; - nodeCachedDataDir?: string; - backupWorkspacesPath: string; -} + constructor(private configuration: ISharedProcessConfiguration) { + super(); -const eventPrefix = 'monacoworkbench'; + // Enable gracefulFs + gracefulify(fs); -class MainProcessService implements IMainProcessService { - - constructor( - private server: Server, - private mainRouter: StaticRouter - ) { } - - declare readonly _serviceBrand: undefined; - - getChannel(channelName: string): IChannel { - return this.server.getChannel(channelName, this.mainRouter); + this.registerListeners(); } - registerChannel(channelName: string, channel: IServerChannel): void { - this.server.registerChannel(channelName, channel); + private registerListeners(): void { + + // Dispose on exit + const onExit = () => this.dispose(); + process.once('exit', onExit); + ipcRenderer.once('vscode:electron-main->shared-process=exit', onExit); } -} -async function main(server: Server, initData: ISharedProcessInitData, configuration: ISharedProcessConfiguration): Promise { - const services = new ServiceCollection(); + async open(): Promise { - const disposables = new DisposableStore(); + // Services + const instantiationService = await this.initServices(); - const onExit = () => disposables.dispose(); - process.once('exit', onExit); - ipcRenderer.once('vscode:electron-main->shared-process=exit', onExit); + // Config + registerUserDataSyncConfiguration(); - disposables.add(server); + instantiationService.invokeFunction(accessor => { + const logService = accessor.get(ILogService); - const environmentService = new NativeEnvironmentService(initData.args); + // Log info + logService.trace('sharedProcess configuration', JSON.stringify(this.configuration)); - const mainRouter = new StaticRouter(ctx => ctx === 'main'); - const loggerClient = new LoggerChannelClient(server.getChannel('logger', mainRouter)); - const logService = new FollowerLogService(loggerClient, new SpdLogService('sharedprocess', environmentService.logsPath, initData.logLevel)); - disposables.add(logService); - logService.info('main', JSON.stringify(configuration)); + // Channels + this.initChannels(accessor); - const mainProcessService = new MainProcessService(server, mainRouter); - services.set(IMainProcessService, mainProcessService); + // Error handler + this.registerErrorHandler(logService); + }); - // Files - const fileService = new FileService(logService); - services.set(IFileService, fileService); - disposables.add(fileService); - const diskFileSystemProvider = new DiskFileSystemProvider(logService); - disposables.add(diskFileSystemProvider); - fileService.registerProvider(Schemas.file, diskFileSystemProvider); + // Instantiate Contributions + this._register(combinedDisposable( + new NodeCachedDataCleaner(this.configuration.nodeCachedDataDir), + instantiationService.createInstance(LanguagePackCachedDataCleaner), + instantiationService.createInstance(StorageDataCleaner, this.configuration.backupWorkspacesPath), + instantiationService.createInstance(LogsDataCleaner), + instantiationService.createInstance(LocalizationsUpdater), + instantiationService.createInstance(DeprecatedExtensionsCleaner) + )); + } - // Configuration - const configurationService = new ConfigurationService(environmentService.settingsResource, fileService); - disposables.add(configurationService); - await configurationService.initialize(); - - // Storage - const storageService = new NativeStorageService(new GlobalStorageDatabaseChannelClient(mainProcessService.getChannel('storage')), logService, environmentService); - await storageService.initialize(); - services.set(IStorageService, storageService); - disposables.add(toDisposable(() => storageService.flush())); - - services.set(IEnvironmentService, environmentService); - services.set(INativeEnvironmentService, environmentService); - - services.set(IProductService, { _serviceBrand: undefined, ...product }); - services.set(ILogService, logService); - services.set(IConfigurationService, configurationService); - services.set(IRequestService, new SyncDescriptor(RequestService)); - services.set(ILoggerService, new SyncDescriptor(LoggerService)); - - const nativeHostService = createChannelSender(mainProcessService.getChannel('nativeHost'), { context: configuration.windowId }); - services.set(INativeHostService, nativeHostService); - const activeWindowManager = new ActiveWindowManager(nativeHostService); - const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id)); - - services.set(IDownloadService, new SyncDescriptor(DownloadService)); - services.set(IExtensionRecommendationNotificationService, new ExtensionRecommendationNotificationServiceChannelClient(server.getChannel('IExtensionRecommendationNotificationService', activeWindowRouter))); - - const instantiationService = new InstantiationService(services); - - let telemetryService: ITelemetryService; - instantiationService.invokeFunction(accessor => { + private async initServices(): Promise { const services = new ServiceCollection(); + + // Environment + const environmentService = new NativeEnvironmentService(this.configuration.args); + services.set(IEnvironmentService, environmentService); + services.set(INativeEnvironmentService, environmentService); + + // Log + const mainRouter = new StaticRouter(ctx => ctx === 'main'); + const loggerClient = new LoggerChannelClient(this.server.getChannel('logger', mainRouter)); // we only use this for log levels + const multiplexLogger = this._register(new MultiplexLogService([ + this._register(new ConsoleLogService(this.configuration.logLevel)), + this._register(new SpdLogService('sharedprocess', environmentService.logsPath, this.configuration.logLevel)) + ])); + + const logService = this._register(new FollowerLogService(loggerClient, multiplexLogger)); + services.set(ILogService, logService); + + // Main Process + const mainProcessService = new MessagePortMainProcessService(this.server, mainRouter); + services.set(IMainProcessService, mainProcessService); + + // Files + const fileService = this._register(new FileService(logService)); + services.set(IFileService, fileService); + + const diskFileSystemProvider = this._register(new DiskFileSystemProvider(logService)); + fileService.registerProvider(Schemas.file, diskFileSystemProvider); + + // Configuration + const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService)); + services.set(IConfigurationService, configurationService); + + await configurationService.initialize(); + + // Storage + const storageService = new NativeStorageService(new GlobalStorageDatabaseChannelClient(mainProcessService.getChannel('storage')), logService, environmentService); + services.set(IStorageService, storageService); + + await storageService.initialize(); + this._register(toDisposable(() => storageService.flush())); + + // Product + services.set(IProductService, { _serviceBrand: undefined, ...product }); + + // Request + services.set(IRequestService, new SyncDescriptor(RequestService)); + + // Native Host + const nativeHostService = createChannelSender(mainProcessService.getChannel('nativeHost'), { context: this.configuration.windowId }); + services.set(INativeHostService, nativeHostService); + + // Download + services.set(IDownloadService, new SyncDescriptor(DownloadService)); + + // Extension recommendations + const activeWindowManager = this._register(new ActiveWindowManager(nativeHostService)); + const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id)); + services.set(IExtensionRecommendationNotificationService, new ExtensionRecommendationNotificationServiceChannelClient(this.server.getChannel('extensionRecommendationNotification', activeWindowRouter))); + + // Logger + const loggerService = this._register(new LoggerService(logService, fileService)); + services.set(ILoggerService, loggerService); + + // Telemetry const { appRoot, extensionsPath, extensionDevelopmentLocationURI, isBuilt, installSourcePath } = environmentService; - let telemetryAppender: ITelemetryAppender = NullAppender; + let telemetryService: ITelemetryService; + let telemetryAppender: ITelemetryAppender; if (!extensionDevelopmentLocationURI && !environmentService.disableTelemetry && product.enableTelemetry) { - telemetryAppender = new TelemetryLogAppender(accessor.get(ILoggerService), environmentService); + telemetryAppender = new TelemetryLogAppender(loggerService, environmentService); + + // Application Insights if (product.aiConfig && product.aiConfig.asimovKey && isBuilt) { - const appInsightsAppender = new AppInsightsAppender(eventPrefix, null, product.aiConfig.asimovKey); - disposables.add(toDisposable(() => appInsightsAppender!.flush())); // Ensure the AI appender is disposed so that it flushes remaining data + const appInsightsAppender = new AppInsightsAppender('monacoworkbench', null, product.aiConfig.asimovKey); + this._register(toDisposable(() => appInsightsAppender.flush())); // Ensure the AI appender is disposed so that it flushes remaining data telemetryAppender = combinedAppender(appInsightsAppender, telemetryAppender); } - const config: ITelemetryServiceConfig = { + + telemetryService = new TelemetryService({ appender: telemetryAppender, - commonProperties: resolveCommonProperties(product.commit, product.version, configuration.machineId, product.msftInternalDomains, installSourcePath), + commonProperties: resolveCommonProperties(fileService, release(), process.arch, product.commit, product.version, this.configuration.machineId, product.msftInternalDomains, installSourcePath), sendErrorTelemetry: true, piiPaths: [appRoot, extensionsPath] - }; - - telemetryService = new TelemetryService(config, configurationService); - services.set(ITelemetryService, telemetryService); + }, configurationService); } else { telemetryService = NullTelemetryService; - services.set(ITelemetryService, NullTelemetryService); + telemetryAppender = NullAppender; } - server.registerChannel('telemetryAppender', new TelemetryAppenderChannel(telemetryAppender)); + this.server.registerChannel('telemetryAppender', new TelemetryAppenderChannel(telemetryAppender)); + services.set(ITelemetryService, telemetryService); + + // Extension Management services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); + + // Extension Gallery services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); - services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService)); - services.set(IDiagnosticsService, new SyncDescriptor(DiagnosticsService)); + + // Extension Tips services.set(IExtensionTipsService, new SyncDescriptor(ExtensionTipsService)); + // Localizations + services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService)); + + // Diagnostics + services.set(IDiagnosticsService, new SyncDescriptor(DiagnosticsService)); + + // Settings Sync services.set(IUserDataSyncAccountService, new SyncDescriptor(UserDataSyncAccountService)); services.set(IUserDataSyncLogService, new SyncDescriptor(UserDataSyncLogService)); - services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(server.getChannel('userDataSyncUtil', client => client.ctx !== 'main'))); + services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(this.server.getChannel('userDataSyncUtil', client => client.ctx !== 'main'))); services.set(IGlobalExtensionEnablementService, new SyncDescriptor(GlobalExtensionEnablementService)); services.set(IIgnoredExtensionsManagementService, new SyncDescriptor(IgnoredExtensionsManagementService)); services.set(IExtensionsStorageSyncService, new SyncDescriptor(ExtensionsStorageSyncService)); @@ -217,114 +255,86 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat services.set(IUserDataAutoSyncEnablementService, new SyncDescriptor(UserDataAutoSyncEnablementService)); services.set(IUserDataSyncResourceEnablementService, new SyncDescriptor(UserDataSyncResourceEnablementService)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService)); - registerConfiguration(); - const instantiationService2 = instantiationService.createChild(services); - - instantiationService2.invokeFunction(accessor => { - - const extensionManagementService = accessor.get(IExtensionManagementService); - const channel = new ExtensionManagementChannel(extensionManagementService, () => null); - server.registerChannel('extensions', channel); - - const localizationsService = accessor.get(ILocalizationsService); - const localizationsChannel = createChannelReceiver(localizationsService); - server.registerChannel('localizations', localizationsChannel); - - const diagnosticsService = accessor.get(IDiagnosticsService); - const diagnosticsChannel = createChannelReceiver(diagnosticsService); - server.registerChannel('diagnostics', diagnosticsChannel); - - const extensionTipsService = accessor.get(IExtensionTipsService); - const extensionTipsChannel = new ExtensionTipsChannel(extensionTipsService); - server.registerChannel('extensionTipsService', extensionTipsChannel); - - const userDataSyncMachinesService = accessor.get(IUserDataSyncMachinesService); - const userDataSyncMachineChannel = new UserDataSyncMachinesServiceChannel(userDataSyncMachinesService); - server.registerChannel('userDataSyncMachines', userDataSyncMachineChannel); - - const authTokenService = accessor.get(IUserDataSyncAccountService); - const authTokenChannel = new UserDataSyncAccountServiceChannel(authTokenService); - server.registerChannel('userDataSyncAccount', authTokenChannel); - - const userDataSyncStoreManagementService = accessor.get(IUserDataSyncStoreManagementService); - const userDataSyncStoreManagementChannel = new UserDataSyncStoreManagementServiceChannel(userDataSyncStoreManagementService); - server.registerChannel('userDataSyncStoreManagement', userDataSyncStoreManagementChannel); - - const userDataSyncService = accessor.get(IUserDataSyncService); - const userDataSyncChannel = new UserDataSyncChannel(server, userDataSyncService, logService); - server.registerChannel('userDataSync', userDataSyncChannel); - - const userDataAutoSync = instantiationService2.createInstance(UserDataAutoSyncService); - const userDataAutoSyncChannel = new UserDataAutoSyncChannel(userDataAutoSync); - server.registerChannel('userDataAutoSync', userDataAutoSyncChannel); - - // clean up deprecated extensions - (extensionManagementService as ExtensionManagementService).removeDeprecatedExtensions(); - // update localizations cache - (localizationsService as LocalizationsService).update(); - // cache clean ups - disposables.add(combinedDisposable( - new NodeCachedDataCleaner(initData.nodeCachedDataDir), - instantiationService2.createInstance(LanguagePackCachedDataCleaner), - instantiationService2.createInstance(StorageDataCleaner, initData.backupWorkspacesPath), - instantiationService2.createInstance(LogsDataCleaner), - userDataAutoSync - )); - disposables.add(extensionManagementService as ExtensionManagementService); - }); - }); -} - -function setupIPC(hook: string): Promise { - function setup(retry: boolean): Promise { - return serve(hook).then(null, err => { - if (!retry || platform.isWindows || err.code !== 'EADDRINUSE') { - return Promise.reject(err); - } - - // should retry, not windows and eaddrinuse - - return connect(hook, '').then( - client => { - // we could connect to a running instance. this is not good, abort - client.dispose(); - return Promise.reject(new Error('There is an instance already running.')); - }, - err => { - // it happens on Linux and OS X that the pipe is left behind - // let's delete it, since we can't connect to it - // and the retry the whole thing - try { - fs.unlinkSync(hook); - } catch (e) { - return Promise.reject(new Error('Error deleting the shared ipc hook.')); - } - - return setup(false); - } - ); - }); + return new InstantiationService(services); } - return setup(true); + private initChannels(accessor: ServicesAccessor): void { + + // Extensions Management + const extensionManagementService = accessor.get(IExtensionManagementService); + const channel = new ExtensionManagementChannel(extensionManagementService, () => null); + this.server.registerChannel('extensions', channel); + + // Localizations + const localizationsService = accessor.get(ILocalizationsService); + const localizationsChannel = createChannelReceiver(localizationsService); + this.server.registerChannel('localizations', localizationsChannel); + + // Diagnostics + const diagnosticsService = accessor.get(IDiagnosticsService); + const diagnosticsChannel = createChannelReceiver(diagnosticsService); + this.server.registerChannel('diagnostics', diagnosticsChannel); + + // Extension Tips + const extensionTipsService = accessor.get(IExtensionTipsService); + const extensionTipsChannel = new ExtensionTipsChannel(extensionTipsService); + this.server.registerChannel('extensionTipsService', extensionTipsChannel); + + // Settings Sync + const userDataSyncMachinesService = accessor.get(IUserDataSyncMachinesService); + const userDataSyncMachineChannel = new UserDataSyncMachinesServiceChannel(userDataSyncMachinesService); + this.server.registerChannel('userDataSyncMachines', userDataSyncMachineChannel); + + const authTokenService = accessor.get(IUserDataSyncAccountService); + const authTokenChannel = new UserDataSyncAccountServiceChannel(authTokenService); + this.server.registerChannel('userDataSyncAccount', authTokenChannel); + + const userDataSyncStoreManagementService = accessor.get(IUserDataSyncStoreManagementService); + const userDataSyncStoreManagementChannel = new UserDataSyncStoreManagementServiceChannel(userDataSyncStoreManagementService); + this.server.registerChannel('userDataSyncStoreManagement', userDataSyncStoreManagementChannel); + + const userDataSyncService = accessor.get(IUserDataSyncService); + const userDataSyncChannel = new UserDataSyncChannel(this.server, userDataSyncService, accessor.get(ILogService)); + this.server.registerChannel('userDataSync', userDataSyncChannel); + + const userDataAutoSync = this._register(accessor.get(IInstantiationService).createInstance(UserDataAutoSyncService)); + const userDataAutoSyncChannel = new UserDataAutoSyncChannel(userDataAutoSync); + this.server.registerChannel('userDataAutoSync', userDataAutoSyncChannel); + } + + private registerErrorHandler(logService: ILogService): void { + + // Listen on unhandled rejection events + window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => { + + // See https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent + onUnexpectedError(event.reason); + + // Prevent the printing of this event to the console + event.preventDefault(); + }); + + // Install handler for unexpected errors + setUnexpectedErrorHandler(error => { + const message = toErrorMessage(error, true); + if (!message) { + return; + } + + logService.error(`[uncaught exception in sharedProcess]: ${message}`); + }); + } } -async function handshake(configuration: ISharedProcessConfiguration): Promise { +export async function main(configuration: ISharedProcessConfiguration): Promise { - // receive payload from electron-main to start things - const data = await new Promise(c => { - ipcRenderer.once('vscode:electron-main->shared-process=payload', (event: unknown, r: ISharedProcessInitData) => c(r)); - - // tell electron-main we are ready to receive payload - ipcRenderer.send('vscode:shared-process->electron-main=ready-for-payload'); - }); - - // await IPC connection and signal this back to electron-main - const server = await setupIPC(data.sharedIPCHandle); + // create shared process and signal back to main that we are + // ready to accept message ports as client connections + const sharedProcess = new SharedProcessMain(configuration); ipcRenderer.send('vscode:shared-process->electron-main=ipc-ready'); // await initialization and signal this back to electron-main - await main(server, data, configuration); + await sharedProcess.open(); ipcRenderer.send('vscode:shared-process->electron-main=init-done'); } diff --git a/src/vs/code/electron-browser/workbench/workbench.html b/src/vs/code/electron-browser/workbench/workbench.html index 40737461..f36737f2 100644 --- a/src/vs/code/electron-browser/workbench/workbench.html +++ b/src/vs/code/electron-browser/workbench/workbench.html @@ -3,7 +3,8 @@ - + + diff --git a/src/vs/code/electron-browser/workbench/workbench.js b/src/vs/code/electron-browser/workbench/workbench.js index 14c69946..64082146 100644 --- a/src/vs/code/electron-browser/workbench/workbench.js +++ b/src/vs/code/electron-browser/workbench/workbench.js @@ -12,8 +12,7 @@ const bootstrapWindow = bootstrapWindowLib(); // Add a perf entry right from the top - const perf = bootstrapWindow.perfLib(); - perf.mark('renderer/started'); + performance.mark('code/didStartRenderer'); // Load workbench main JS, CSS and NLS all in parallel. This is an // optimization to prevent a waterfall of loading to happen, because @@ -24,10 +23,10 @@ 'vs/nls!vs/workbench/workbench.desktop.main', 'vs/css!vs/workbench/workbench.desktop.main' ], - async function (workbench, configuration) { + function (_, configuration) { // Mark start of workbench - perf.mark('didLoadWorkbenchMain'); + performance.mark('code/didLoadWorkbenchMain'); // @ts-ignore return require('vs/workbench/electron-browser/desktop.main').main(configuration); @@ -41,19 +40,34 @@ loaderConfig.recordStats = true; }, beforeRequire: function () { - perf.mark('willLoadWorkbenchMain'); + performance.mark('code/willLoadWorkbenchMain'); } } ); + // add default trustedTypes-policy for logging and to workaround + // lib/platform limitations + window.trustedTypes?.createPolicy('default', { + createHTML(value) { + // see https://github.com/electron/electron/issues/27211 + // Electron webviews use a static innerHTML default value and + // that isn't trusted. We use a default policy to check for the + // exact value of that innerHTML-string and only allow that. + if (value === '') { + return value; + } + throw new Error('UNTRUSTED html usage, default trusted types policy should NEVER be reached'); + // console.trace('UNTRUSTED html usage, default trusted types policy should NEVER be reached'); + // return value; + } + }); //region Helpers /** * @returns {{ - * load: (modules: string[], resultCallback: (result, configuration: object) => any, options: object) => unknown, - * globals: () => typeof import('../../../base/parts/sandbox/electron-sandbox/globals'), - * perfLib: () => { mark: (name: string) => void } + * load: (modules: string[], resultCallback: (result, configuration: import('../../../platform/windows/common/windows').INativeWindowConfiguration) => any, options: object) => unknown, + * globals: () => typeof import('../../../base/parts/sandbox/electron-sandbox/globals') * }} */ function bootstrapWindowLib() { @@ -67,12 +81,11 @@ * colorScheme: ('light' | 'dark' | 'hc'), * autoDetectHighContrast?: boolean, * extensionDevelopmentPath?: string[], - * folderUri?: object, - * workspace?: object + * workspace?: import('../../../platform/workspaces/common/workspaces').IWorkspaceIdentifier | import('../../../platform/workspaces/common/workspaces').ISingleFolderWorkspaceIdentifier * }} configuration */ function showPartsSplash(configuration) { - perf.mark('willShowPartsSplash'); + performance.mark('code/willShowPartsSplash'); let data; if (typeof configuration.partsSplashPath === 'string') { @@ -147,7 +160,7 @@ splash.appendChild(activityDiv); // part: side bar (only when opening workspace/folder) - if (configuration.folderUri || configuration.workspace) { + if (configuration.workspace) { // folder or workspace -> status bar color, sidebar const sideDiv = document.createElement('div'); sideDiv.setAttribute('style', `position: absolute; height: calc(100% - ${layoutInfo.titleBarHeight}px); top: ${layoutInfo.titleBarHeight}px; ${layoutInfo.sideBarSide}: ${layoutInfo.activityBarWidth}px; width: ${layoutInfo.sideBarWidth}px; background-color: ${colorInfo.sideBarBackground};`); @@ -156,13 +169,13 @@ // part: statusbar const statusDiv = document.createElement('div'); - statusDiv.setAttribute('style', `position: absolute; width: 100%; bottom: 0; left: 0; height: ${layoutInfo.statusBarHeight}px; background-color: ${configuration.folderUri || configuration.workspace ? colorInfo.statusBarBackground : colorInfo.statusBarNoFolderBackground};`); + statusDiv.setAttribute('style', `position: absolute; width: 100%; bottom: 0; left: 0; height: ${layoutInfo.statusBarHeight}px; background-color: ${configuration.workspace ? colorInfo.statusBarBackground : colorInfo.statusBarNoFolderBackground};`); splash.appendChild(statusDiv); document.body.appendChild(splash); } - perf.mark('didShowPartsSplash'); + performance.mark('code/didShowPartsSplash'); } //#endregion diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 759c85bc..3f85072d 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -4,17 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { app, ipcMain, systemPreferences, contentTracing, protocol, BrowserWindow, dialog, session } from 'electron'; -import { IProcessEnvironment, isWindows, isMacintosh, isLinux } from 'vs/base/common/platform'; +import { release } from 'os'; +import { IProcessEnvironment, isWindows, isMacintosh, isLinux, isLinuxSnap } from 'vs/base/common/platform'; import { WindowsMainService } from 'vs/platform/windows/electron-main/windowsMainService'; import { IWindowOpenable } from 'vs/platform/windows/common/windows'; -import { OpenContext } from 'vs/platform/windows/node/window'; import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { resolveShellEnv } from 'vs/code/node/shellEnv'; import { IUpdateService } from 'vs/platform/update/common/update'; import { UpdateChannel } from 'vs/platform/update/electron-main/updateIpc'; -import { Server as ElectronIPCServer } from 'vs/base/parts/ipc/electron-main/ipc.electron-main'; -import { Client } from 'vs/base/parts/ipc/common/ipc.net'; -import { Server, connect } from 'vs/base/parts/ipc/node/ipc.net'; +import { getDelayedChannel, StaticRouter, createChannelReceiver, createChannelSender } from 'vs/base/parts/ipc/common/ipc'; +import { Server as ElectronIPCServer } from 'vs/base/parts/ipc/electron-main/ipc.electron'; +import { Server as NodeIPCServer } from 'vs/base/parts/ipc/node/ipc.net'; +import { Client as MessagePortClient } from 'vs/base/parts/ipc/electron-main/ipc.mp'; import { SharedProcess } from 'vs/code/electron-main/sharedProcess'; import { LaunchMainService, ILaunchMainService } from 'vs/platform/launch/electron-main/launchMainService'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -28,19 +29,17 @@ import { IOpenURLOptions, IURLService } from 'vs/platform/url/common/url'; import { URLHandlerChannelClient, URLHandlerRouter } from 'vs/platform/url/common/urlIpc'; import { ITelemetryService, machineIdKey } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc'; +import { TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc'; import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; -import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; -import { getDelayedChannel, StaticRouter, createChannelReceiver, createChannelSender } from 'vs/base/parts/ipc/common/ipc'; +import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties'; import product from 'vs/platform/product/common/product'; import { ProxyAuthHandler } from 'vs/code/electron-main/auth'; -import { ProxyAuthHandler2 } from 'vs/code/electron-main/auth2'; import { FileProtocolHandler } from 'vs/code/electron-main/protocol'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, ICodeWindow, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { URI } from 'vs/base/common/uri'; import { hasWorkspaceFileExtension, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; -import { WorkspacesService } from 'vs/platform/workspaces/electron-main/workspacesService'; +import { WorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; import { getMachineId } from 'vs/base/node/id'; import { Win32UpdateService } from 'vs/platform/update/electron-main/updateService.win32'; import { LinuxUpdateService } from 'vs/platform/update/electron-main/updateService.linux'; @@ -64,14 +63,13 @@ import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainSe import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; import { WorkspacesHistoryMainService, IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; import { NativeURLService } from 'vs/platform/url/common/urlService'; -import { WorkspacesMainService, IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; +import { WorkspacesManagementMainService, IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { statSync } from 'fs'; import { IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; import { ElectronExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/electron-main/extensionHostDebugIpc'; import { INativeHostMainService, NativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; -import { ISharedProcessMainService, SharedProcessMainService } from 'vs/platform/ipc/electron-main/sharedProcessMainService'; -import { IDialogMainService, DialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { IDialogMainService, DialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { withNullAsUndefined } from 'vs/base/common/types'; import { mnemonicButtonLabel, getPathLabel } from 'vs/base/common/labels'; import { WebviewMainService } from 'vs/platform/webview/electron-main/webviewMainService'; @@ -81,7 +79,7 @@ import { stripComments } from 'vs/base/common/json'; import { generateUuid } from 'vs/base/common/uuid'; import { VSBuffer } from 'vs/base/common/buffer'; import { EncryptionMainService, IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService'; -import { ActiveWindowManager } from 'vs/platform/windows/common/windowTracker'; +import { ActiveWindowManager } from 'vs/platform/windows/node/windowTracker'; import { IKeyboardLayoutMainService, KeyboardLayoutMainService } from 'vs/platform/keyboardLayout/electron-main/keyboardLayoutMainService'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { DisplayMainService, IDisplayMainService } from 'vs/platform/display/electron-main/displayMainService'; @@ -90,6 +88,7 @@ import { isEqualOrParent } from 'vs/base/common/extpath'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IExtensionUrlTrustService } from 'vs/platform/extensionManagement/common/extensionUrlTrust'; import { ExtensionUrlTrustService } from 'vs/platform/extensionManagement/node/extensionUrlTrustService'; +import { once } from 'vs/base/common/functional'; export class CodeApplication extends Disposable { private windowsMainService: IWindowsMainService | undefined; @@ -97,7 +96,7 @@ export class CodeApplication extends Disposable { private nativeHostMainService: INativeHostMainService | undefined; constructor( - private readonly mainIpcServer: Server, + private readonly mainIpcServer: NodeIPCServer, private readonly userEnv: IProcessEnvironment, @IInstantiationService private readonly instantiationService: IInstantiationService, @ILogService private readonly logService: ILogService, @@ -150,19 +149,19 @@ export class CodeApplication extends Disposable { event.preventDefault(); }); app.on('remote-get-global', (event, sender, module) => { - this.logService.trace(`App#on(remote-get-global): prevented on ${module}`); + this.logService.trace(`app#on(remote-get-global): prevented on ${module}`); event.preventDefault(); }); app.on('remote-get-builtin', (event, sender, module) => { - this.logService.trace(`App#on(remote-get-builtin): prevented on ${module}`); + this.logService.trace(`app#on(remote-get-builtin): prevented on ${module}`); if (module !== 'clipboard') { event.preventDefault(); } }); app.on('remote-get-current-window', event => { - this.logService.trace(`App#on(remote-get-current-window): prevented`); + this.logService.trace(`app#on(remote-get-current-window): prevented`); event.preventDefault(); }); @@ -171,7 +170,7 @@ export class CodeApplication extends Disposable { return; // the driver needs access to web contents } - this.logService.trace(`App#on(remote-get-current-web-contents): prevented`); + this.logService.trace(`app#on(remote-get-current-web-contents): prevented`); event.preventDefault(); }); @@ -271,9 +270,13 @@ export class CodeApplication extends Disposable { //#region Bootstrap IPC Handlers + let slowShellResolveWarningShown = false; ipcMain.on('vscode:fetchShellEnv', async event => { + + // DO NOT remove: not only usual windows are fetching the + // shell environment but also shared process, issue reporter + // etc, so we need to reply via `webContents` always const webContents = event.sender; - const window = this.windowsMainService?.getWindowByWebContents(event.sender); let replied = false; @@ -291,11 +294,19 @@ export class CodeApplication extends Disposable { } // Handle slow shell environment resolve calls: - // - a warning after 3s but continue to resolve - // - an error after 10s and stop trying to resolve + // - a warning after 3s but continue to resolve (only once in active window) + // - an error after 10s and stop trying to resolve (in every window where this happens) const cts = new CancellationTokenSource(); - const shellEnvSlowWarningHandle = setTimeout(() => window?.sendWhenReady('vscode:showShellEnvSlowWarning', cts.token), 3000); - const shellEnvTimeoutErrorHandle = setTimeout(function () { + + const shellEnvSlowWarningHandle = setTimeout(() => { + if (!slowShellResolveWarningShown) { + this.windowsMainService?.sendToFocused('vscode:showShellEnvSlowWarning', cts.token); + slowShellResolveWarningShown = true; + } + }, 3000); + + const window = this.windowsMainService?.getWindowByWebContents(event.sender); // Note: this can be `undefined` for the shared process!! + const shellEnvTimeoutErrorHandle = setTimeout(() => { cts.dispose(true); window?.sendWhenReady('vscode:showShellEnvTimeoutError', CancellationToken.None); acceptShellEnv({}); @@ -304,8 +315,11 @@ export class CodeApplication extends Disposable { // Prefer to use the args and env from the target window // when resolving the shell env. It is possible that // a first window was opened from the UI but a second - // from the CLI and that has implications for wether to + // from the CLI and that has implications for whether to // resolve the shell environment or not. + // + // Window can be undefined for e.g. the shared process + // that is not part of our windows registry! let args: NativeParsedArgs; let env: NodeJS.ProcessEnv; if (window?.config) { @@ -321,10 +335,10 @@ export class CodeApplication extends Disposable { acceptShellEnv(shellEnv); }); - ipcMain.handle('vscode:writeNlsFile', (event, path: unknown, data: unknown) => { + ipcMain.handle('vscode:writeNlsFile', async (event, path: unknown, data: unknown) => { const uri = this.validateNlsPath([path]); if (!uri || typeof data !== 'string') { - return Promise.reject('Invalid operation (vscode:writeNlsFile)'); + throw new Error('Invalid operation (vscode:writeNlsFile)'); } return this.fileService.writeFile(uri, VSBuffer.fromString(data)); @@ -333,7 +347,7 @@ export class CodeApplication extends Disposable { ipcMain.handle('vscode:readNlsFile', async (event, ...paths: unknown[]) => { const uri = this.validateNlsPath(paths); if (!uri) { - return Promise.reject('Invalid operation (vscode:readNlsFile)'); + throw new Error('Invalid operation (vscode:readNlsFile)'); } return (await this.fileService.readFile(uri)).value.toString(); @@ -427,16 +441,20 @@ export class CodeApplication extends Disposable { // Spawn shared process after the first window has opened and 3s have passed const sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv); - const sharedProcessClient = sharedProcess.whenIpcReady().then(() => { - this.logService.trace('Shared process: IPC ready'); + const sharedProcessClient = (async () => { + this.logService.trace('Main->SharedProcess#connect'); - return connect(this.environmentService.sharedIPCHandle, 'main'); - }); - const sharedProcessReady = sharedProcess.whenReady().then(() => { - this.logService.trace('Shared process: init ready'); + const port = await sharedProcess.connect(); + + this.logService.trace('Main->SharedProcess#connect: connection established'); + + return new MessagePortClient(port, 'main'); + })(); + const sharedProcessReady = (async () => { + await sharedProcess.whenReady(); return sharedProcessClient; - }); + })(); this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => { this._register(new RunOnceScheduler(async () => { sharedProcess.spawn(await resolveShellEnv(this.logService, this.environmentService.args, process.env)); @@ -454,12 +472,8 @@ export class CodeApplication extends Disposable { this._register(server); } - // Setup Auth Handler (TODO@ben remove old auth handler eventually) - if (this.configurationService.getValue('window.enableExperimentalProxyLoginDialog') === false) { - this._register(new ProxyAuthHandler()); - } else { - this._register(appInstantiationService.createInstance(ProxyAuthHandler2)); - } + // Setup Auth Handler + this._register(appInstantiationService.createInstance(ProxyAuthHandler)); // Open Windows const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient, fileProtocolHandler)); @@ -487,7 +501,7 @@ export class CodeApplication extends Disposable { return machineId; } - private async createServices(machineId: string, sharedProcess: SharedProcess, sharedProcessReady: Promise>): Promise { + private async createServices(machineId: string, sharedProcess: SharedProcess, sharedProcessReady: Promise): Promise { const services = new ServiceCollection(); switch (process.platform) { @@ -496,8 +510,8 @@ export class CodeApplication extends Disposable { break; case 'linux': - if (process.env.SNAP && process.env.SNAP_REVISION) { - services.set(IUpdateService, new SyncDescriptor(SnapUpdateService, [process.env.SNAP, process.env.SNAP_REVISION])); + if (isLinuxSnap) { + services.set(IUpdateService, new SyncDescriptor(SnapUpdateService, [process.env['SNAP'], process.env['SNAP_REVISION']])); } else { services.set(IUpdateService, new SyncDescriptor(LinuxUpdateService)); } @@ -510,7 +524,6 @@ export class CodeApplication extends Disposable { services.set(IWindowsMainService, new SyncDescriptor(WindowsMainService, [machineId, this.userEnv])); services.set(IDialogMainService, new SyncDescriptor(DialogMainService)); - services.set(ISharedProcessMainService, new SyncDescriptor(SharedProcessMainService, [sharedProcess])); services.set(ILaunchMainService, new SyncDescriptor(LaunchMainService)); services.set(IDiagnosticsService, createChannelSender(getDelayedChannel(sharedProcessReady.then(client => client.getChannel('diagnostics'))))); @@ -518,9 +531,9 @@ export class CodeApplication extends Disposable { services.set(IEncryptionMainService, new SyncDescriptor(EncryptionMainService, [machineId])); services.set(IKeyboardLayoutMainService, new SyncDescriptor(KeyboardLayoutMainService)); services.set(IDisplayMainService, new SyncDescriptor(DisplayMainService)); - services.set(INativeHostMainService, new SyncDescriptor(NativeHostMainService)); + services.set(INativeHostMainService, new SyncDescriptor(NativeHostMainService, [sharedProcess])); services.set(IWebviewManagerService, new SyncDescriptor(WebviewMainService)); - services.set(IWorkspacesService, new SyncDescriptor(WorkspacesService)); + services.set(IWorkspacesService, new SyncDescriptor(WorkspacesMainService)); services.set(IMenubarMainService, new SyncDescriptor(MenubarMainService)); services.set(IExtensionUrlTrustService, new SyncDescriptor(ExtensionUrlTrustService)); @@ -533,13 +546,13 @@ export class CodeApplication extends Disposable { services.set(IWorkspacesHistoryMainService, new SyncDescriptor(WorkspacesHistoryMainService)); services.set(IURLService, new SyncDescriptor(NativeURLService)); - services.set(IWorkspacesMainService, new SyncDescriptor(WorkspacesMainService)); + services.set(IWorkspacesManagementMainService, new SyncDescriptor(WorkspacesManagementMainService)); // Telemetry if (!this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { const channel = getDelayedChannel(sharedProcessReady.then(client => client.getChannel('telemetryAppender'))); const appender = new TelemetryAppenderClient(channel); - const commonProperties = resolveCommonProperties(product.commit, product.version, machineId, product.msftInternalDomains, this.environmentService.installSourcePath); + const commonProperties = resolveCommonProperties(this.fileService, release(), process.arch, product.commit, product.version, machineId, product.msftInternalDomains, this.environmentService.installSourcePath); const piiPaths = [this.environmentService.appRoot, this.environmentService.extensionsPath]; const config: ITelemetryServiceConfig = { appender, commonProperties, piiPaths, sendErrorTelemetry: true }; @@ -589,7 +602,7 @@ export class CodeApplication extends Disposable { }); } - private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise>, fileProtocolHandler: FileProtocolHandler): ICodeWindow[] { + private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise, fileProtocolHandler: FileProtocolHandler): ICodeWindow[] { // Register more Main IPC services const launchMainService = accessor.get(ILaunchMainService); @@ -622,10 +635,6 @@ export class CodeApplication extends Disposable { electronIpcServer.registerChannel('nativeHost', nativeHostChannel); sharedProcessClient.then(client => client.registerChannel('nativeHost', nativeHostChannel)); - const sharedProcessMainService = accessor.get(ISharedProcessMainService); - const sharedProcessChannel = createChannelReceiver(sharedProcessMainService); - electronIpcServer.registerChannel('sharedProcess', sharedProcessChannel); - const workspacesService = accessor.get(IWorkspacesService); const workspacesChannel = createChannelReceiver(workspacesService); electronIpcServer.registerChannel('workspaces', workspacesChannel); @@ -705,6 +714,8 @@ export class CodeApplication extends Disposable { }); // Create a URL handler to open file URIs in the active window + // or open new windows. The URL handler will be invoked from + // protocol invocations outside of VSCode. const app = this; const environmentService = this.environmentService; urlService.registerHandler({ @@ -718,13 +729,15 @@ export class CodeApplication extends Disposable { // Check for URIs to open in window const windowOpenableFromProtocolLink = app.getWindowOpenableFromProtocolLink(uri); if (windowOpenableFromProtocolLink) { - windowsMainService.open({ + const [window] = windowsMainService.open({ context: OpenContext.API, cli: { ...environmentService.args }, urisToOpen: [windowOpenableFromProtocolLink], gotoLineMode: true }); + window.focus(); // this should help ensuring that the right window gets focus when multiple are opened + return true; } @@ -832,7 +845,7 @@ export class CodeApplication extends Disposable { mnemonicButtonLabel(localize({ key: 'cancel', comment: ['&& denotes a mnemonic'] }, "&&No")), ], cancelId: 1, - message: localize('confirmOpenMessage', "An external application wants to open '{0}' in {1}. Do you want to open this file or folder?", getPathLabel(uri.fsPath), product.nameShort), + message: localize('confirmOpenMessage', "An external application wants to open '{0}' in {1}. Do you want to open this file or folder?", getPathLabel(uri.fsPath, this.environmentService), product.nameShort), detail: localize('confirmOpenDetail', "If you did not initiate this request, it may represent an attempted attack on your system. Unless you took an explicit action to initiate this request, you should press 'No'"), noLink: true }); @@ -901,8 +914,25 @@ export class CodeApplication extends Disposable { // Signal phase: after window open this.lifecycleMainService.phase = LifecycleMainPhase.AfterWindowOpen; + // Windows: install mutex + const win32MutexName = product.win32MutexName; + if (isWindows && win32MutexName) { + try { + const WindowsMutex = (require.__$__nodeRequire('windows-mutex') as typeof import('windows-mutex')).Mutex; + const mutex = new WindowsMutex(win32MutexName); + once(this.lifecycleMainService.onWillShutdown)(() => mutex.release()); + } catch (error) { + this.logService.error(error); + } + } + // Remote Authorities - this.handleRemoteAuthorities(); + protocol.registerHttpProtocol(Schemas.vscodeRemoteResource, (request, callback) => { + callback({ + url: request.url.replace(/^vscode-remote-resource:/, 'http:'), + method: request.method + }); + }); // Initialize update service const updateService = accessor.get(IUpdateService); @@ -934,19 +964,11 @@ export class CodeApplication extends Disposable { '}' ]; const newArgvString = argvString.substring(0, argvString.length - 2).concat(',\n', additionalArgvContent.join('\n')); + await this.fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(newArgvString)); } } catch (error) { this.logService.error(error); } } - - private handleRemoteAuthorities(): void { - protocol.registerHttpProtocol(Schemas.vscodeRemoteResource, (request, callback) => { - callback({ - url: request.url.replace(/^vscode-remote-resource:/, 'http:'), - method: request.method - }); - }); - } } diff --git a/src/vs/code/electron-main/auth.ts b/src/vs/code/electron-main/auth.ts index b4096018..b9669f98 100644 --- a/src/vs/code/electron-main/auth.ts +++ b/src/vs/code/electron-main/auth.ts @@ -3,18 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { Disposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; -import { FileAccess } from 'vs/base/common/network'; -import { BrowserWindow, BrowserWindowConstructorOptions, app, AuthInfo, WebContents, Event as ElectronEvent } from 'electron'; +import { hash } from 'vs/base/common/hash'; +import { app, AuthInfo, WebContents, Event as ElectronEvent, AuthenticationResponseDetails } from 'electron'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; +import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; +import { IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService'; +import { generateUuid } from 'vs/base/common/uuid'; +import product from 'vs/platform/product/common/product'; +import { CancellationToken } from 'vs/base/common/cancellation'; + +interface ElectronAuthenticationResponseDetails extends AuthenticationResponseDetails { + firstAuthAttempt?: boolean; // https://github.com/electron/electron/blob/84a42a050e7d45225e69df5bd2d2bf9f1037ea41/shell/browser/login_handler.cc#L70 +} type LoginEvent = { event: ElectronEvent; - webContents: WebContents; - req: Request; authInfo: AuthInfo; - cb: (username: string, password: string) => void; + req: ElectronAuthenticationResponseDetails; + + callback: (username?: string, password?: string) => void; }; type Credentials = { @@ -22,81 +32,211 @@ type Credentials = { password: string; }; +enum ProxyAuthState { + + /** + * Initial state: we will try to use stored credentials + * first to reply to the auth challenge. + */ + Initial = 1, + + /** + * We used stored credentials and are still challenged, + * so we will show a login dialog next. + */ + StoredCredentialsUsed, + + /** + * Finally, if we showed a login dialog already, we will + * not show any more login dialogs until restart to reduce + * the UI noise. + */ + LoginDialogShown +} + export class ProxyAuthHandler extends Disposable { - declare readonly _serviceBrand: undefined; + private static PROXY_CREDENTIALS_SERVICE_KEY = `${product.urlProtocol}.proxy-credentials`; - private retryCount = 0; + private pendingProxyResolve: Promise | undefined = undefined; - constructor() { + private state = ProxyAuthState.Initial; + + private sessionCredentials: Credentials | undefined = undefined; + + constructor( + @ILogService private readonly logService: ILogService, + @IWindowsMainService private readonly windowsMainService: IWindowsMainService, + @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, + @IEncryptionMainService private readonly encryptionMainService: IEncryptionMainService + ) { super(); this.registerListeners(); } private registerListeners(): void { - const onLogin = Event.fromNodeEventEmitter(app, 'login', (event, webContents, req, authInfo, cb) => ({ event, webContents, req, authInfo, cb })); + const onLogin = Event.fromNodeEventEmitter(app, 'login', (event: ElectronEvent, webContents: WebContents, req: ElectronAuthenticationResponseDetails, authInfo: AuthInfo, callback) => ({ event, webContents, req, authInfo, callback })); this._register(onLogin(this.onLogin, this)); } - private onLogin({ event, authInfo, cb }: LoginEvent): void { + private async onLogin({ event, authInfo, req, callback }: LoginEvent): Promise { if (!authInfo.isProxy) { - return; + return; // only for proxy } - if (this.retryCount++ > 1) { - return; + if (!this.pendingProxyResolve && this.state === ProxyAuthState.LoginDialogShown && req.firstAuthAttempt) { + this.logService.trace('auth#onLogin (proxy) - exit - proxy dialog already shown'); + + return; // only one dialog per session at max (except when firstAuthAttempt: false which indicates a login problem) } + // Signal we handle this event on our own, otherwise + // Electron will ignore our provided credentials. event.preventDefault(); - const opts: BrowserWindowConstructorOptions = { - alwaysOnTop: true, - skipTaskbar: true, - resizable: false, - width: 450, - height: 225, - show: true, - title: 'VS Code', - webPreferences: { - preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, - sandbox: true, - contextIsolation: true, - enableWebSQL: false, - enableRemoteModule: false, - spellcheck: false, - devTools: false - } - }; + let credentials: Credentials | undefined = undefined; + if (!this.pendingProxyResolve) { + this.logService.trace('auth#onLogin (proxy) - no pending proxy handling found, starting new'); - const focusedWindow = BrowserWindow.getFocusedWindow(); - if (focusedWindow) { - opts.parent = focusedWindow; - opts.modal = true; + this.pendingProxyResolve = this.resolveProxyCredentials(authInfo); + try { + credentials = await this.pendingProxyResolve; + } finally { + this.pendingProxyResolve = undefined; + } + } else { + this.logService.trace('auth#onLogin (proxy) - pending proxy handling found'); + + credentials = await this.pendingProxyResolve; } - const win = new BrowserWindow(opts); - const windowUrl = FileAccess.asBrowserUri('vs/code/electron-sandbox/proxy/auth.html', require); - const proxyUrl = `${authInfo.host}:${authInfo.port}`; - const title = localize('authRequire', "Proxy Authentication Required"); - const message = localize('proxyauth', "The proxy {0} requires authentication.", proxyUrl); + // According to Electron docs, it is fine to call back without + // username or password to signal that the authentication was handled + // by us, even though without having credentials received: + // + // > If `callback` is called without a username or password, the authentication + // > request will be cancelled and the authentication error will be returned to the + // > page. + callback(credentials?.username, credentials?.password); + } - const onWindowClose = () => cb('', ''); - win.on('close', onWindowClose); + private async resolveProxyCredentials(authInfo: AuthInfo): Promise { + this.logService.trace('auth#resolveProxyCredentials (proxy) - enter'); - win.setMenu(null); - win.webContents.on('did-finish-load', () => { - const data = { title, message }; - win.webContents.send('vscode:openProxyAuthDialog', data); - }); - win.webContents.on('ipc-message', (event, channel, credentials: Credentials) => { - if (channel === 'vscode:proxyAuthResponse') { - const { username, password } = credentials; - cb(username, password); - win.removeListener('close', onWindowClose); - win.close(); + try { + const credentials = await this.doResolveProxyCredentials(authInfo); + if (credentials) { + this.logService.trace('auth#resolveProxyCredentials (proxy) - got credentials'); + + return credentials; + } else { + this.logService.trace('auth#resolveProxyCredentials (proxy) - did not get credentials'); } + } finally { + this.logService.trace('auth#resolveProxyCredentials (proxy) - exit'); + } + + return undefined; + } + + private async doResolveProxyCredentials(authInfo: AuthInfo): Promise { + this.logService.trace('auth#doResolveProxyCredentials - enter', authInfo); + + // Compute a hash over the authentication info to be used + // with the credentials store to return the right credentials + // given the properties of the auth request + // (see https://github.com/microsoft/vscode/issues/109497) + const authInfoHash = String(hash({ scheme: authInfo.scheme, host: authInfo.host, port: authInfo.port })); + + // Find any previously stored credentials + let storedUsername: string | undefined = undefined; + let storedPassword: string | undefined = undefined; + try { + const encryptedSerializedProxyCredentials = await this.nativeHostMainService.getPassword(undefined, ProxyAuthHandler.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); + if (encryptedSerializedProxyCredentials) { + const credentials: Credentials = JSON.parse(await this.encryptionMainService.decrypt(encryptedSerializedProxyCredentials)); + + storedUsername = credentials.username; + storedPassword = credentials.password; + } + } catch (error) { + this.logService.error(error); // handle errors by asking user for login via dialog + } + + // Reply with stored credentials unless we used them already. + // In that case we need to show a login dialog again because + // they seem invalid. + if (this.state !== ProxyAuthState.StoredCredentialsUsed && typeof storedUsername === 'string' && typeof storedPassword === 'string') { + this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - found stored credentials to use'); + this.state = ProxyAuthState.StoredCredentialsUsed; + + return { username: storedUsername, password: storedPassword }; + } + + // Find suitable window to show dialog: prefer to show it in the + // active window because any other network request will wait on + // the credentials and we want the user to present the dialog. + const window = this.windowsMainService.getFocusedWindow() || this.windowsMainService.getLastActiveWindow(); + if (!window) { + this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - no opened window found to show dialog in'); + + return undefined; // unexpected + } + + this.logService.trace(`auth#doResolveProxyCredentials (proxy) - asking window ${window.id} to handle proxy login`); + + // Open proxy dialog + const payload = { + authInfo, + username: this.sessionCredentials?.username ?? storedUsername, // prefer to show already used username (if any) over stored + password: this.sessionCredentials?.password ?? storedPassword, // prefer to show already used password (if any) over stored + replyChannel: `vscode:proxyAuthResponse:${generateUuid()}` + }; + window.sendWhenReady('vscode:openProxyAuthenticationDialog', CancellationToken.None, payload); + this.state = ProxyAuthState.LoginDialogShown; + + // Handle reply + const loginDialogCredentials = await new Promise(resolve => { + const proxyAuthResponseHandler = async (event: ElectronEvent, channel: string, reply: Credentials & { remember: boolean } | undefined /* canceled */) => { + if (channel === payload.replyChannel) { + this.logService.trace(`auth#doResolveProxyCredentials - exit - received credentials from window ${window.id}`); + window.win.webContents.off('ipc-message', proxyAuthResponseHandler); + + // We got credentials from the window + if (reply) { + const credentials: Credentials = { username: reply.username, password: reply.password }; + + // Update stored credentials based on `remember` flag + try { + if (reply.remember) { + const encryptedSerializedCredentials = await this.encryptionMainService.encrypt(JSON.stringify(credentials)); + await this.nativeHostMainService.setPassword(undefined, ProxyAuthHandler.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash, encryptedSerializedCredentials); + } else { + await this.nativeHostMainService.deletePassword(undefined, ProxyAuthHandler.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); + } + } catch (error) { + this.logService.error(error); // handle gracefully + } + + resolve({ username: credentials.username, password: credentials.password }); + } + + // We did not get any credentials from the window (e.g. cancelled) + else { + resolve(undefined); + } + } + }; + + window.win.webContents.on('ipc-message', proxyAuthResponseHandler); }); - win.loadURL(windowUrl.toString(true)); + + // Remember credentials for the session in case + // the credentials are wrong and we show the dialog + // again + this.sessionCredentials = loginDialogCredentials; + + return loginDialogCredentials; } } diff --git a/src/vs/code/electron-main/auth2.ts b/src/vs/code/electron-main/auth2.ts deleted file mode 100644 index 1b84d4bc..00000000 --- a/src/vs/code/electron-main/auth2.ts +++ /dev/null @@ -1,242 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable } from 'vs/base/common/lifecycle'; -import { Event } from 'vs/base/common/event'; -import { hash } from 'vs/base/common/hash'; -import { app, AuthInfo, WebContents, Event as ElectronEvent, AuthenticationResponseDetails } from 'electron'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; -import { IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService'; -import { generateUuid } from 'vs/base/common/uuid'; -import product from 'vs/platform/product/common/product'; -import { CancellationToken } from 'vs/base/common/cancellation'; - -interface ElectronAuthenticationResponseDetails extends AuthenticationResponseDetails { - firstAuthAttempt?: boolean; // https://github.com/electron/electron/blob/84a42a050e7d45225e69df5bd2d2bf9f1037ea41/shell/browser/login_handler.cc#L70 -} - -type LoginEvent = { - event: ElectronEvent; - authInfo: AuthInfo; - req: ElectronAuthenticationResponseDetails; - - callback: (username?: string, password?: string) => void; -}; - -type Credentials = { - username: string; - password: string; -}; - -enum ProxyAuthState { - - /** - * Initial state: we will try to use stored credentials - * first to reply to the auth challenge. - */ - Initial = 1, - - /** - * We used stored credentials and are still challenged, - * so we will show a login dialog next. - */ - StoredCredentialsUsed, - - /** - * Finally, if we showed a login dialog already, we will - * not show any more login dialogs until restart to reduce - * the UI noise. - */ - LoginDialogShown -} - -export class ProxyAuthHandler2 extends Disposable { - - private static PROXY_CREDENTIALS_SERVICE_KEY = `${product.urlProtocol}.proxy-credentials`; - - private pendingProxyResolve: Promise | undefined = undefined; - - private state = ProxyAuthState.Initial; - - private sessionCredentials: Credentials | undefined = undefined; - - constructor( - @ILogService private readonly logService: ILogService, - @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, - @IEncryptionMainService private readonly encryptionMainService: IEncryptionMainService - ) { - super(); - - this.registerListeners(); - } - - private registerListeners(): void { - const onLogin = Event.fromNodeEventEmitter(app, 'login', (event: ElectronEvent, webContents: WebContents, req: ElectronAuthenticationResponseDetails, authInfo: AuthInfo, callback) => ({ event, webContents, req, authInfo, callback })); - this._register(onLogin(this.onLogin, this)); - } - - private async onLogin({ event, authInfo, req, callback }: LoginEvent): Promise { - if (!authInfo.isProxy) { - return; // only for proxy - } - - if (!this.pendingProxyResolve && this.state === ProxyAuthState.LoginDialogShown && req.firstAuthAttempt) { - this.logService.trace('auth#onLogin (proxy) - exit - proxy dialog already shown'); - - return; // only one dialog per session at max (except when firstAuthAttempt: false which indicates a login problem) - } - - // Signal we handle this event on our own, otherwise - // Electron will ignore our provided credentials. - event.preventDefault(); - - let credentials: Credentials | undefined = undefined; - if (!this.pendingProxyResolve) { - this.logService.trace('auth#onLogin (proxy) - no pending proxy handling found, starting new'); - - this.pendingProxyResolve = this.resolveProxyCredentials(authInfo); - try { - credentials = await this.pendingProxyResolve; - } finally { - this.pendingProxyResolve = undefined; - } - } else { - this.logService.trace('auth#onLogin (proxy) - pending proxy handling found'); - - credentials = await this.pendingProxyResolve; - } - - // According to Electron docs, it is fine to call back without - // username or password to signal that the authentication was handled - // by us, even though without having credentials received: - // - // > If `callback` is called without a username or password, the authentication - // > request will be cancelled and the authentication error will be returned to the - // > page. - callback(credentials?.username, credentials?.password); - } - - private async resolveProxyCredentials(authInfo: AuthInfo): Promise { - this.logService.trace('auth#resolveProxyCredentials (proxy) - enter'); - - try { - const credentials = await this.doResolveProxyCredentials(authInfo); - if (credentials) { - this.logService.trace('auth#resolveProxyCredentials (proxy) - got credentials'); - - return credentials; - } else { - this.logService.trace('auth#resolveProxyCredentials (proxy) - did not get credentials'); - } - } finally { - this.logService.trace('auth#resolveProxyCredentials (proxy) - exit'); - } - - return undefined; - } - - private async doResolveProxyCredentials(authInfo: AuthInfo): Promise { - this.logService.trace('auth#doResolveProxyCredentials - enter', authInfo); - - // Compute a hash over the authentication info to be used - // with the credentials store to return the right credentials - // given the properties of the auth request - // (see https://github.com/microsoft/vscode/issues/109497) - const authInfoHash = String(hash({ scheme: authInfo.scheme, host: authInfo.host, port: authInfo.port })); - - // Find any previously stored credentials - let storedUsername: string | undefined = undefined; - let storedPassword: string | undefined = undefined; - try { - const encryptedSerializedProxyCredentials = await this.nativeHostMainService.getPassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); - if (encryptedSerializedProxyCredentials) { - const credentials: Credentials = JSON.parse(await this.encryptionMainService.decrypt(encryptedSerializedProxyCredentials)); - - storedUsername = credentials.username; - storedPassword = credentials.password; - } - } catch (error) { - this.logService.error(error); // handle errors by asking user for login via dialog - } - - // Reply with stored credentials unless we used them already. - // In that case we need to show a login dialog again because - // they seem invalid. - if (this.state !== ProxyAuthState.StoredCredentialsUsed && typeof storedUsername === 'string' && typeof storedPassword === 'string') { - this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - found stored credentials to use'); - this.state = ProxyAuthState.StoredCredentialsUsed; - - return { username: storedUsername, password: storedPassword }; - } - - // Find suitable window to show dialog: prefer to show it in the - // active window because any other network request will wait on - // the credentials and we want the user to present the dialog. - const window = this.windowsMainService.getFocusedWindow() || this.windowsMainService.getLastActiveWindow(); - if (!window) { - this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - no opened window found to show dialog in'); - - return undefined; // unexpected - } - - this.logService.trace(`auth#doResolveProxyCredentials (proxy) - asking window ${window.id} to handle proxy login`); - - // Open proxy dialog - const payload = { - authInfo, - username: this.sessionCredentials?.username ?? storedUsername, // prefer to show already used username (if any) over stored - password: this.sessionCredentials?.password ?? storedPassword, // prefer to show already used password (if any) over stored - replyChannel: `vscode:proxyAuthResponse:${generateUuid()}` - }; - window.sendWhenReady('vscode:openProxyAuthenticationDialog', CancellationToken.None, payload); - this.state = ProxyAuthState.LoginDialogShown; - - // Handle reply - const loginDialogCredentials = await new Promise(resolve => { - const proxyAuthResponseHandler = async (event: ElectronEvent, channel: string, reply: Credentials & { remember: boolean } | undefined /* canceled */) => { - if (channel === payload.replyChannel) { - this.logService.trace(`auth#doResolveProxyCredentials - exit - received credentials from window ${window.id}`); - window.win.webContents.off('ipc-message', proxyAuthResponseHandler); - - // We got credentials from the window - if (reply) { - const credentials: Credentials = { username: reply.username, password: reply.password }; - - // Update stored credentials based on `remember` flag - try { - if (reply.remember) { - const encryptedSerializedCredentials = await this.encryptionMainService.encrypt(JSON.stringify(credentials)); - await this.nativeHostMainService.setPassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash, encryptedSerializedCredentials); - } else { - await this.nativeHostMainService.deletePassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); - } - } catch (error) { - this.logService.error(error); // handle gracefully - } - - resolve({ username: credentials.username, password: credentials.password }); - } - - // We did not get any credentials from the window (e.g. cancelled) - else { - resolve(undefined); - } - } - }; - - window.win.webContents.on('ipc-message', proxyAuthResponseHandler); - }); - - // Remember credentials for the session in case - // the credentials are wrong and we show the dialog - // again - this.sessionCredentials = loginDialogCredentials; - - return loginDialogCredentials; - } -} diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 783ac6fb..f0d8b6d2 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -5,15 +5,17 @@ import 'vs/platform/update/common/update.config.contribution'; import { app, dialog } from 'electron'; -import * as fs from 'fs'; +import { unlinkSync } from 'fs'; +import { localize } from 'vs/nls'; import { isWindows, IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; import product from 'vs/platform/product/common/product'; import { parseMainProcessArgv, addArg } from 'vs/platform/environment/node/argvHelper'; import { createWaitMarkerFile } from 'vs/platform/environment/node/waitMarkerFile'; import { mkdirp } from 'vs/base/node/pfs'; import { LifecycleMainService, ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; -import { Server, serve, connect, XDG_RUNTIME_DIR } from 'vs/base/parts/ipc/node/ipc.net'; import { createChannelSender } from 'vs/base/parts/ipc/common/ipc'; +import { Server as NodeIPCServer, serve as nodeIPCServe, connect as nodeIPCConnect, XDG_RUNTIME_DIR } from 'vs/base/parts/ipc/node/ipc.net'; +import { Client as NodeIPCClient } from 'vs/base/parts/ipc/common/ipc.net'; import { ILaunchMainService } from 'vs/platform/launch/electron-main/launchMainService'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; @@ -29,17 +31,15 @@ import { ConfigurationService } from 'vs/platform/configuration/common/configura import { IRequestService } from 'vs/platform/request/common/request'; import { RequestMainService } from 'vs/platform/request/electron-main/requestMainService'; import { CodeApplication } from 'vs/code/electron-main/app'; -import { localize } from 'vs/nls'; -import { mnemonicButtonLabel } from 'vs/base/common/labels'; +import { getPathLabel, mnemonicButtonLabel } from 'vs/base/common/labels'; import { SpdLogService } from 'vs/platform/log/node/spdlogService'; import { BufferLogService } from 'vs/platform/log/common/bufferLog'; import { setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { IThemeMainService, ThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; -import { Client } from 'vs/base/parts/ipc/common/ipc.net'; import { once } from 'vs/base/common/functional'; import { ISignService } from 'vs/platform/sign/common/sign'; import { SignService } from 'vs/platform/sign/node/signService'; -import { IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; +import { DiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; import { FileService } from 'vs/platform/files/common/fileService'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; @@ -53,6 +53,8 @@ import { rtrim, trim } from 'vs/base/common/strings'; import { basename, resolve } from 'vs/base/common/path'; import { coalesce, distinct } from 'vs/base/common/arrays'; import { EnvironmentMainService, IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; class ExpectedError extends Error { readonly isExpected = true; @@ -212,14 +214,14 @@ class CodeMain { return instanceEnvironment; } - private async doStartup(args: NativeParsedArgs, logService: ILogService, environmentService: IEnvironmentMainService, lifecycleMainService: ILifecycleMainService, instantiationService: IInstantiationService, retry: boolean): Promise { + private async doStartup(args: NativeParsedArgs, logService: ILogService, environmentService: IEnvironmentMainService, lifecycleMainService: ILifecycleMainService, instantiationService: IInstantiationService, retry: boolean): Promise { // Try to setup a server for running. If that succeeds it means // we are the first instance to startup. Otherwise it is likely // that another instance is already running. - let server: Server; + let server: NodeIPCServer; try { - server = await serve(environmentService.mainIPCHandle); + server = await nodeIPCServe(environmentService.mainIPCHandle); once(lifecycleMainService.onWillShutdown)(() => server.dispose()); } catch (error) { @@ -235,9 +237,9 @@ class CodeMain { } // there's a running instance, let's connect to it - let client: Client; + let client: NodeIPCClient; try { - client = await connect(environmentService.mainIPCHandle, 'main'); + client = await nodeIPCConnect(environmentService.mainIPCHandle, 'main'); } catch (error) { // Handle unexpected connection errors by showing a dialog to the user @@ -256,7 +258,7 @@ class CodeMain { // let's delete it, since we can't connect to it and then // retry the whole thing try { - fs.unlinkSync(environmentService.mainIPCHandle); + unlinkSync(environmentService.mainIPCHandle); } catch (error) { logService.warn('Could not delete obsolete instance handle', error); @@ -293,11 +295,7 @@ class CodeMain { // Process Info if (args.status) { return instantiationService.invokeFunction(async () => { - - // Create a diagnostic service connected to the existing shared process - const sharedProcessClient = await connect(environmentService.sharedIPCHandle, 'main'); - const diagnosticsChannel = sharedProcessClient.getChannel('diagnostics'); - const diagnosticsService = createChannelSender(diagnosticsChannel); + const diagnosticsService = new DiagnosticsService(NullTelemetryService); const mainProcessInfo = await launchService.getMainProcessInfo(); const remoteDiagnostics = await launchService.getRemoteDiagnostics({ includeProcesses: true, includeWorkspaceMetadata: true }); const diagnostics = await diagnosticsService.getDiagnostics(mainProcessInfo, remoteDiagnostics); @@ -343,11 +341,11 @@ class CodeMain { private handleStartupDataDirError(environmentService: IEnvironmentMainService, error: NodeJS.ErrnoException): void { if (error.code === 'EACCES' || error.code === 'EPERM') { - const directories = coalesce([environmentService.userDataPath, environmentService.extensionsPath, XDG_RUNTIME_DIR]); + const directories = coalesce([environmentService.userDataPath, environmentService.extensionsPath, XDG_RUNTIME_DIR]).map(folder => getPathLabel(folder, environmentService)); this.showStartupWarningDialog( localize('startupDataDirError', "Unable to write program user data."), - localize('startupUserDataAndExtensionsDirErrorDetail', "Please make sure the following directories are writeable:\n\n{0}", directories.join('\n')) + localize('startupUserDataAndExtensionsDirErrorDetail', "{0}\n\nPlease make sure the following directories are writeable:\n\n{1}", toErrorMessage(error), directories.join('\n')) ); } } diff --git a/src/vs/code/electron-main/protocol.ts b/src/vs/code/electron-main/protocol.ts index 36d09319..5bfbebb6 100644 --- a/src/vs/code/electron-main/protocol.ts +++ b/src/vs/code/electron-main/protocol.ts @@ -11,7 +11,7 @@ import { INativeEnvironmentService } from 'vs/platform/environment/common/enviro import { session } from 'electron'; import { ILogService } from 'vs/platform/log/common/log'; import { TernarySearchTree } from 'vs/base/common/map'; -import { isLinux } from 'vs/base/common/platform'; +import { isLinux, isPreferringBrowserCodeLoad } from 'vs/base/common/platform'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; type ProtocolCallback = { (result: string | Electron.FilePathWithHeaders | { error: number }): void }; @@ -37,15 +37,17 @@ export class FileProtocolHandler extends Disposable { // Register vscode-file:// handler defaultSession.protocol.registerFileProtocol(Schemas.vscodeFileResource, (request, callback) => this.handleResourceRequest(request, callback as unknown as ProtocolCallback)); - // Block any file:// access (sandbox only) - if (environmentService.args.__sandbox) { + // Block any file:// access (explicitly enabled only) + if (isPreferringBrowserCodeLoad) { + this.logService.info(`Intercepting ${Schemas.file}: protocol and blocking it`); + defaultSession.protocol.interceptFileProtocol(Schemas.file, (request, callback) => this.handleFileRequest(request, callback as unknown as ProtocolCallback)); } // Cleanup this._register(toDisposable(() => { defaultSession.protocol.unregisterProtocol(Schemas.vscodeFileResource); - if (environmentService.args.__sandbox) { + if (isPreferringBrowserCodeLoad) { defaultSession.protocol.uninterceptProtocol(Schemas.file); } })); diff --git a/src/vs/code/electron-main/sharedProcess.ts b/src/vs/code/electron-main/sharedProcess.ts index dd721db9..812a4dd1 100644 --- a/src/vs/code/electron-main/sharedProcess.ts +++ b/src/vs/code/electron-main/sharedProcess.ts @@ -3,25 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { memoize } from 'vs/base/common/decorators'; +import { BrowserWindow, ipcMain, Event, MessagePortMain } from 'electron'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; -import { BrowserWindow, ipcMain, WebContents, Event as ElectronEvent } from 'electron'; -import { ISharedProcess } from 'vs/platform/ipc/electron-main/sharedProcessMainService'; import { Barrier } from 'vs/base/common/async'; import { ILogService } from 'vs/platform/log/common/log'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; -import { toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { Event } from 'vs/base/common/event'; import { FileAccess } from 'vs/base/common/network'; +import { browserCodeLoadingCacheStrategy } from 'vs/base/common/platform'; +import { ISharedProcess, ISharedProcessConfiguration } from 'vs/platform/sharedProcess/node/sharedProcess'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { connect as connectMessagePort } from 'vs/base/parts/ipc/electron-main/ipc.mp'; +import { assertIsDefined } from 'vs/base/common/types'; -export class SharedProcess implements ISharedProcess { +export class SharedProcess extends Disposable implements ISharedProcess { - private barrier = new Barrier(); + private readonly whenSpawnedBarrier = new Barrier(); - private window: BrowserWindow | null = null; - - private readonly _whenReady: Promise; + private window: BrowserWindow | undefined = undefined; + private windowCloseListener: ((event: Event) => void) | undefined = undefined; constructor( private readonly machineId: string, @@ -31,17 +31,125 @@ export class SharedProcess implements ISharedProcess { @ILogService private readonly logService: ILogService, @IThemeMainService private readonly themeMainService: IThemeMainService ) { - // overall ready promise when shared process signals initialization is done - this._whenReady = new Promise(c => ipcMain.once('vscode:shared-process->electron-main=init-done', () => c(undefined))); + super(); + + this.registerListeners(); } - @memoize - private get _whenIpcReady(): Promise { + private registerListeners(): void { + + // Lifecycle + this._register(this.lifecycleMainService.onWillShutdown(() => this.onWillShutdown())); + + // Shared process connections from workbench windows + ipcMain.on('vscode:createSharedProcessMessageChannel', async (e, nonce: string) => { + this.logService.trace('SharedProcess: on vscode:createSharedProcessMessageChannel'); + + // await the shared process to be overall ready + // we do not just wait for IPC ready because the + // workbench window will communicate directly + await this.whenReady(); + + // connect to the shared process window + const port = await this.connect(); + + // Check back if the requesting window meanwhile closed + // Since shared process is delayed on startup there is + // a chance that the window close before the shared process + // was ready for a connection. + if (e.sender.isDestroyed()) { + return port.close(); + } + + // send the port back to the requesting window + e.sender.postMessage('vscode:createSharedProcessMessageChannelResult', nonce, [port]); + }); + } + + private onWillShutdown(): void { + const window = this.window; + if (!window) { + return; // possibly too early before created + } + + // Signal exit to shared process when shutting down + if (!window.isDestroyed() && !window.webContents.isDestroyed()) { + window.webContents.send('vscode:electron-main->shared-process=exit'); + } + + // Shut the shared process down when we are quitting + // + // Note: because we veto the window close, we must first remove our veto. + // Otherwise the application would never quit because the shared process + // window is refusing to close! + // + if (this.windowCloseListener) { + window.removeListener('close', this.windowCloseListener); + this.windowCloseListener = undefined; + } + + // Electron seems to crash on Windows without this setTimeout :| + setTimeout(() => { + try { + window.close(); + } catch (err) { + // ignore, as electron is already shutting down + } + + this.window = undefined; + }, 0); + } + + private _whenReady: Promise | undefined = undefined; + whenReady(): Promise { + if (!this._whenReady) { + // Overall signal that the shared process window was loaded and + // all services within have been created. + this._whenReady = new Promise(resolve => ipcMain.once('vscode:shared-process->electron-main=init-done', () => { + this.logService.trace('SharedProcess: Overall ready'); + + resolve(); + })); + } + + return this._whenReady; + } + + private _whenIpcReady: Promise | undefined = undefined; + private get whenIpcReady() { + if (!this._whenIpcReady) { + this._whenIpcReady = (async () => { + + // Always wait for `spawn()` + await this.whenSpawnedBarrier.wait(); + + // Create window for shared process + this.createWindow(); + + // Listeners + this.registerWindowListeners(); + + // Wait for window indicating that IPC connections are accepted + await new Promise(resolve => ipcMain.once('vscode:shared-process->electron-main=ipc-ready', () => { + this.logService.trace('SharedProcess: IPC ready'); + + resolve(); + })); + })(); + } + + return this._whenIpcReady; + } + + private createWindow(): void { + + // shared process is a hidden window by default this.window = new BrowserWindow({ show: false, backgroundColor: this.themeMainService.getBackgroundColor(), webPreferences: { preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, + v8CacheOptions: browserCodeLoadingCacheStrategy, nodeIntegration: true, enableWebSQL: false, enableRemoteModule: false, @@ -52,118 +160,85 @@ export class SharedProcess implements ISharedProcess { disableBlinkFeatures: 'Auxclick' // do NOT change, allows us to identify this window as shared-process in the process explorer } }); - const config = { - appRoot: this.environmentService.appRoot, + + const config: ISharedProcessConfiguration = { machineId: this.machineId, + windowId: this.window.id, + appRoot: this.environmentService.appRoot, nodeCachedDataDir: this.environmentService.nodeCachedDataDir, + backupWorkspacesPath: this.environmentService.backupWorkspacesPath, userEnv: this.userEnv, - windowId: this.window.id + sharedIPCHandle: this.environmentService.sharedIPCHandle, + args: this.environmentService.args, + logLevel: this.logService.getLevel() }; - const windowUrl = (this.environmentService.sandbox ? - FileAccess._asCodeFileUri('vs/code/electron-browser/sharedProcess/sharedProcess.html', require) : - FileAccess.asBrowserUri('vs/code/electron-browser/sharedProcess/sharedProcess.html', require)) - .with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` }); - this.window.loadURL(windowUrl.toString(true)); + // Load with config + this.window.loadURL(FileAccess + .asBrowserUri('vs/code/electron-browser/sharedProcess/sharedProcess.html', require) + .with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` }) + .toString(true) + ); + } - // Prevent the window from dying - const onClose = (e: ElectronEvent) => { + private registerWindowListeners(): void { + if (!this.window) { + return; + } + + // Prevent the window from closing + this.windowCloseListener = (e: Event) => { this.logService.trace('SharedProcess#close prevented'); // We never allow to close the shared process unless we get explicitly disposed() e.preventDefault(); // Still hide the window though if visible - if (this.window && this.window.isVisible()) { + if (this.window?.isVisible()) { this.window.hide(); } }; - this.window.on('close', onClose); + this.window.on('close', this.windowCloseListener); - const disposables = new DisposableStore(); - - this.lifecycleMainService.onWillShutdown(() => { - disposables.dispose(); - - // Shut the shared process down when we are quitting - // - // Note: because we veto the window close, we must first remove our veto. - // Otherwise the application would never quit because the shared process - // window is refusing to close! - // - if (this.window) { - this.window.removeListener('close', onClose); - } - - // Electron seems to crash on Windows without this setTimeout :| - setTimeout(() => { - try { - if (this.window) { - this.window.close(); - } - } catch (err) { - // ignore, as electron is already shutting down - } - - this.window = null; - }, 0); - }); - - return new Promise(c => { - // send payload once shared process is ready to receive it - disposables.add(Event.once(Event.fromNodeEventEmitter(ipcMain, 'vscode:shared-process->electron-main=ready-for-payload', ({ sender }: { sender: WebContents }) => sender))(sender => { - sender.send('vscode:electron-main->shared-process=payload', { - sharedIPCHandle: this.environmentService.sharedIPCHandle, - args: this.environmentService.args, - logLevel: this.logService.getLevel(), - backupWorkspacesPath: this.environmentService.backupWorkspacesPath, - nodeCachedDataDir: this.environmentService.nodeCachedDataDir - }); - - // signal exit to shared process when we get disposed - disposables.add(toDisposable(() => sender.send('vscode:electron-main->shared-process=exit'))); - - // complete IPC-ready promise when shared process signals this to us - ipcMain.once('vscode:shared-process->electron-main=ipc-ready', () => c(undefined)); - })); - }); + // Crashes & Unrsponsive & Failed to load + this.window.webContents.on('render-process-gone', (event, details) => this.logService.error(`SharedProcess: crashed (detail: ${details?.reason})`)); + this.window.on('unresponsive', () => this.logService.error('SharedProcess: detected unresponsive window')); + this.window.webContents.on('did-fail-load', (event, errorCode, errorDescription) => this.logService.warn('SharedProcess: failed to load window, ', errorDescription)); } spawn(userEnv: NodeJS.ProcessEnv): void { this.userEnv = { ...this.userEnv, ...userEnv }; - this.barrier.open(); + + // Release barrier + this.whenSpawnedBarrier.open(); } - async whenReady(): Promise { - await this.barrier.wait(); - await this._whenReady; + async connect(): Promise { + + // Wait for shared process being ready to accept connection + await this.whenIpcReady; + + // Connect and return message port + const window = assertIsDefined(this.window); + return connectMessagePort(window); } - async whenIpcReady(): Promise { - await this.barrier.wait(); - await this._whenIpcReady; - } + async toggle(): Promise { - toggle(): void { - if (!this.window || this.window.isVisible()) { - this.hide(); - } else { - this.show(); + // wait for window to be created + await this.whenIpcReady; + + if (!this.window) { + return; // possibly disposed already } - } - show(): void { - if (this.window) { + if (this.window.isVisible()) { + this.window.webContents.closeDevTools(); + this.window.hide(); + } else { this.window.show(); this.window.webContents.openDevTools(); } } - - hide(): void { - if (this.window) { - this.window.webContents.closeDevTools(); - this.window.hide(); - } - } } diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index a9f47be8..6ce9e1dc 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as os from 'os'; -import * as path from 'vs/base/common/path'; -import * as nls from 'vs/nls'; -import * as perf from 'vs/base/common/performance'; +import { release } from 'os'; +import { join } from 'vs/base/common/path'; +import { localize } from 'vs/nls'; +import { getMarks, mark } from 'vs/base/common/performance'; import { 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, nativeTheme, Event, Details } from 'electron'; +import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment, nativeTheme, Event, RenderProcessGoneDetails } from 'electron'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { ILogService } from 'vs/platform/log/common/log'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -18,17 +18,17 @@ import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import product from 'vs/platform/product/common/product'; import { WindowMinimumSize, IWindowSettings, MenuBarVisibility, getTitleBarStyle, getMenuBarVisibility, zoomLevelToZoomFactor, INativeWindowConfiguration } from 'vs/platform/windows/common/windows'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; +import { browserCodeLoadingCacheStrategy, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { ICodeWindow, IWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows'; -import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; -import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; +import { ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; import { RunOnceScheduler } from 'vs/base/common/async'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; @@ -102,32 +102,32 @@ export class CodeWindow extends Disposable implements ICodeWindow { private hiddenTitleBarStyle: boolean | undefined; private showTimeoutHandle: NodeJS.Timeout | undefined; - private _lastFocusTime: number; - private _readyState: ReadyState; + private _lastFocusTime = -1; + private _readyState = ReadyState.NONE; private windowState: IWindowState; private currentMenuBarVisibility: MenuBarVisibility | undefined; private representedFilename: string | undefined; private documentEdited: boolean | undefined; - private readonly whenReadyCallbacks: { (window: ICodeWindow): void }[]; + private readonly whenReadyCallbacks: { (window: ICodeWindow): void }[] = []; private marketplaceHeadersPromise: Promise; - private readonly touchBarGroups: TouchBarSegmentedControl[]; + private readonly touchBarGroups: TouchBarSegmentedControl[] = []; - private currentHttpProxy?: string; - private currentNoProxy?: string; + private currentHttpProxy: string | undefined = undefined; + private currentNoProxy: string | undefined = undefined; constructor( config: IWindowCreationOptions, @ILogService private readonly logService: ILogService, @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, @IFileService private readonly fileService: IFileService, - @IStorageMainService private readonly storageService: IStorageMainService, + @IStorageMainService storageService: IStorageMainService, @IConfigurationService private readonly configurationService: IConfigurationService, @IThemeMainService private readonly themeMainService: IThemeMainService, - @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, + @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @IBackupMainService private readonly backupMainService: IBackupMainService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IDialogMainService private readonly dialogMainService: IDialogMainService, @@ -135,11 +135,6 @@ export class CodeWindow extends Disposable implements ICodeWindow { ) { super(); - this.touchBarGroups = []; - this._lastFocusTime = -1; - this._readyState = ReadyState.NONE; - this.whenReadyCallbacks = []; - //#region create browser window { // Load window state @@ -164,6 +159,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { title: product.nameLong, webPreferences: { preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, + v8CacheOptions: browserCodeLoadingCacheStrategy, enableWebSQL: false, enableRemoteModule: false, spellcheck: false, @@ -185,13 +181,17 @@ export class CodeWindow extends Disposable implements ICodeWindow { } }; + if (browserCodeLoadingCacheStrategy) { + this.logService.info(`window#ctor: using vscode-file protocol and V8 cache options: ${browserCodeLoadingCacheStrategy}`); + } + // Apply icon to window // Linux: always // Windows: only when running out of sources, otherwise an icon is set by us on the executable if (isLinux) { - options.icon = path.join(this.environmentService.appRoot, 'resources/linux/code.png'); + options.icon = join(this.environmentService.appRoot, 'resources/linux/code.png'); } else if (isWindows && !this.environmentService.isBuilt) { - options.icon = path.join(this.environmentService.appRoot, 'resources/win32/code_150x150.png'); + options.icon = join(this.environmentService.appRoot, 'resources/win32/code_150x150.png'); } if (isMacintosh && !this.useNativeFullScreen()) { @@ -233,7 +233,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { this._win.setSheetOffset(22); // offset dialogs by the height of the custom title bar if we have any } - // TODO@Ben (Electron 4 regression): when running on multiple displays where the target display + // TODO@bpasero (Electron 4 regression): when running on multiple displays where the target display // to open the window has a larger resolution than the primary display, the window will not size // correctly unless we set the bounds again (https://github.com/microsoft/vscode/issues/74872) // @@ -275,10 +275,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.createTouchBar(); // Request handling - const that = this; this.marketplaceHeadersPromise = resolveMarketplaceHeaders(product.version, this.environmentService, this.fileService, { - get(key) { return that.storageService.get(key); }, - store(key, value) { that.storageService.store(key, value); } + get(key) { return storageService.get(key); }, + store(key, value) { storageService.store(key, value); } }); // Eventing @@ -361,13 +360,11 @@ export class CodeWindow extends Disposable implements ICodeWindow { get lastFocusTime(): number { return this._lastFocusTime; } - get backupPath(): string | undefined { return this.currentConfig ? this.currentConfig.backupPath : undefined; } + get backupPath(): string | undefined { return this.currentConfig?.backupPath; } - get openedWorkspace(): IWorkspaceIdentifier | undefined { return this.currentConfig ? this.currentConfig.workspace : undefined; } + get openedWorkspace(): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined { return this.currentConfig?.workspace; } - get openedFolderUri(): URI | undefined { return this.currentConfig ? this.currentConfig.folderUri : undefined; } - - get remoteAuthority(): string | undefined { return this.currentConfig ? this.currentConfig.remoteAuthority : undefined; } + get remoteAuthority(): string | undefined { return this.currentConfig?.remoteAuthority; } setReady(): void { this._readyState = ReadyState.READY; @@ -413,9 +410,10 @@ export class CodeWindow extends Disposable implements ICodeWindow { private registerListeners(): void { - // Crashes & Unrsponsive + // Crashes & Unrsponsive & Failed to load this._win.webContents.on('render-process-gone', (event, details) => this.onWindowError(WindowError.CRASHED, details)); this._win.on('unresponsive', () => this.onWindowError(WindowError.UNRESPONSIVE)); + this._win.webContents.on('did-fail-load', (event, errorCode, errorDescription) => this.logService.warn('Main: failed to load workbench window, ', errorDescription)); // Window close this._win.on('closed', () => { @@ -424,24 +422,42 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.dispose(); }); - // Prevent loading of svgs - this._win.webContents.session.webRequest.onBeforeRequest(null!, (details, callback) => { - if (details.url.indexOf('.svg') > 0) { - const uri = URI.parse(details.url); - if (uri && !uri.scheme.match(/file/i) && uri.path.endsWith('.svg')) { + const svgFileSchemes = new Set([Schemas.file, Schemas.vscodeFileResource, Schemas.vscodeRemoteResource, 'devtools']); + this._win.webContents.session.webRequest.onBeforeRequest((details, callback) => { + const uri = URI.parse(details.url); + // Prevent loading of remote svgs + if (uri && uri.path.endsWith('.svg')) { + const safeScheme = svgFileSchemes.has(uri.scheme) || + uri.path.includes(Schemas.vscodeRemoteResource); + if (!safeScheme) { return callback({ cancel: true }); } } - return callback({}); + return callback({ cancel: false }); }); - this._win.webContents.session.webRequest.onHeadersReceived(null!, (details, callback) => { + this._win.webContents.session.webRequest.onHeadersReceived((details, callback) => { const responseHeaders = details.responseHeaders as Record; - 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 }); + + if (contentType && Array.isArray(contentType)) { + const uri = URI.parse(details.url); + // https://github.com/microsoft/vscode/issues/97564 + // ensure local svg files have Content-Type image/svg+xml + if (uri && uri.path.endsWith('.svg')) { + if (svgFileSchemes.has(uri.scheme)) { + responseHeaders['Content-Type'] = ['image/svg+xml']; + return callback({ cancel: false, responseHeaders }); + } + } + + // remote extension schemes have the following format + // http://127.0.0.1:/vscode-remote-resource?path= + if (!uri.path.includes(Schemas.vscodeRemoteResource) && + contentType.some(x => x.toLowerCase().includes('image/svg'))) { + return callback({ cancel: true }); + } } return callback({ cancel: false }); @@ -526,16 +542,11 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.sendWhenReady('vscode:leaveFullScreen', CancellationToken.None); }); - // Window Failed to load - this._win.webContents.on('did-fail-load', (event: Event, errorCode: number, errorDescription: string, validatedURL: string, isMainFrame: boolean) => { - this.logService.warn('[electron event]: fail to load, ', errorDescription); - }); - // Handle configuration changes this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated())); // Handle Workspace events - this._register(this.workspacesMainService.onUntitledWorkspaceDeleted(e => this.onUntitledWorkspaceDeleted(e))); + this._register(this.workspacesManagementMainService.onUntitledWorkspaceDeleted(e => this.onUntitledWorkspaceDeleted(e))); // Inject headers when requests are incoming const urls = ['https://marketplace.visualstudio.com/*', 'https://*.vsassets.io/*']; @@ -544,9 +555,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { } private onWindowError(error: WindowError.UNRESPONSIVE): void; - private onWindowError(error: WindowError.CRASHED, details: Details): void; - private onWindowError(error: WindowError, details?: Details): void { - this.logService.error(error === WindowError.CRASHED ? `[VS Code]: renderer process crashed (detail: ${details?.reason})` : '[VS Code]: detected unresponsive'); + private onWindowError(error: WindowError.CRASHED, details: RenderProcessGoneDetails): void; + private onWindowError(error: WindowError, details?: RenderProcessGoneDetails): void { + this.logService.error(error === WindowError.CRASHED ? `Main: renderer process crashed (detail: ${details?.reason})` : 'Main: detected unresponsive'); // If we run extension tests from CLI, showing a dialog is not // very helpful in this case. Rather, we bring down the test run @@ -568,7 +579,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Unresponsive if (error === WindowError.UNRESPONSIVE) { if (this.isExtensionDevelopmentHost || this.isExtensionTestHost || (this._win && this._win.webContents && this._win.webContents.isDevToolsOpened())) { - // TODO@Ben Workaround for https://github.com/microsoft/vscode/issues/56994 + // TODO@bpasero Workaround for https://github.com/microsoft/vscode/issues/56994 // In certain cases the window can report unresponsiveness because a breakpoint was hit // and the process is stopped executing. The most typical cases are: // - devtools are opened and debugging happens @@ -581,9 +592,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.dialogMainService.showMessageBox({ title: product.nameLong, type: 'warning', - buttons: [mnemonicButtonLabel(nls.localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(nls.localize({ key: 'wait', comment: ['&& denotes a mnemonic'] }, "&&Keep Waiting")), mnemonicButtonLabel(nls.localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))], - message: nls.localize('appStalled', "The window is no longer responding"), - detail: nls.localize('appStalledDetail', "You can reopen or close the window or keep waiting."), + buttons: [mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(localize({ key: 'wait', comment: ['&& denotes a mnemonic'] }, "&&Keep Waiting")), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))], + message: localize('appStalled', "The window is no longer responding"), + detail: localize('appStalledDetail', "You can reopen or close the window or keep waiting."), noLink: true }, this._win).then(result => { if (!this._win) { @@ -591,6 +602,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { } if (result.response === 0) { + this._win.webContents.forcefullyCrashRenderer(); // Calling reload() immediately after calling this method will force the reload to occur in a new process this.reload(); } else if (result.response === 2) { this.destroyWindow(); @@ -602,17 +614,17 @@ export class CodeWindow extends Disposable implements ICodeWindow { else { let message: string; if (details && details.reason !== 'crashed') { - message = nls.localize('appCrashedDetails', "The window has crashed (reason: '{0}')", details?.reason); + message = localize('appCrashedDetails', "The window has crashed (reason: '{0}')", details?.reason); } else { - message = nls.localize('appCrashed', "The window has crashed", details?.reason); + message = localize('appCrashed', "The window has crashed", details?.reason); } this.dialogMainService.showMessageBox({ title: product.nameLong, type: 'warning', - buttons: [mnemonicButtonLabel(nls.localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(nls.localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))], + buttons: [mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))], message, - detail: nls.localize('appCrashedDetail', "We are sorry for the inconvenience! You can reopen the window to continue where you left off."), + detail: localize('appCrashedDetail', "We are sorry for the inconvenience! You can reopen the window to continue where you left off."), noLink: true }, this._win).then(result => { if (!this._win) { @@ -643,31 +655,32 @@ export class CodeWindow extends Disposable implements ICodeWindow { } private onConfigurationUpdated(): void { + + // Menubar const newMenuBarVisibility = this.getMenuBarVisibility(); if (newMenuBarVisibility !== this.currentMenuBarVisibility) { this.currentMenuBarVisibility = newMenuBarVisibility; this.setMenuBarVisibility(newMenuBarVisibility); } - // Do not set to empty configuration at startup if setting is empty to not override configuration through CLI options: - const env = process.env; + + // Proxy let newHttpProxy = (this.configurationService.getValue('http.proxy') || '').trim() - || (env.https_proxy || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.HTTP_PROXY || '').trim() // Not standardized. + || (process.env['https_proxy'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['HTTP_PROXY'] || '').trim() // Not standardized. || undefined; + if (newHttpProxy?.endsWith('/')) { newHttpProxy = newHttpProxy.substr(0, newHttpProxy.length - 1); } - const newNoProxy = (env.no_proxy || env.NO_PROXY || '').trim() || undefined; // Not standardized. + + const newNoProxy = (process.env['no_proxy'] || process.env['NO_PROXY'] || '').trim() || undefined; // Not standardized. if ((newHttpProxy || '').indexOf('@') === -1 && (newHttpProxy !== this.currentHttpProxy || newNoProxy !== this.currentNoProxy)) { this.currentHttpProxy = newHttpProxy; this.currentNoProxy = newNoProxy; + const proxyRules = newHttpProxy || ''; const proxyBypassRules = newNoProxy ? `${newNoProxy},` : ''; this.logService.trace(`Setting proxy to '${proxyRules}', bypassing '${proxyBypassRules}'`); - this._win.webContents.session.setProxy({ - proxyRules, - proxyBypassRules, - pacScript: '', - }); + this._win.webContents.session.setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); } } @@ -677,7 +690,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { } } - load(config: INativeWindowConfiguration, isReload?: boolean, disableExtensions?: boolean): void { + load(config: INativeWindowConfiguration, { isReload, disableExtensions }: { isReload?: boolean, disableExtensions?: boolean } = Object.create(null)): void { // If this window was loaded before from the command line // (as indicated by VSCODE_CLI environment), make sure to @@ -689,6 +702,15 @@ export class CodeWindow extends Disposable implements ICodeWindow { config.userEnv = { ...currentUserEnv, ...config.userEnv }; // still allow to override certain environment as passed in } + // If named pipe was instantiated for the crashpad_handler process, reuse the same + // pipe for new app instances connecting to the original app instance. + // Ref: https://github.com/microsoft/vscode/issues/115874 + if (process.env['CHROME_CRASHPAD_PIPE_NAME']) { + Object.assign(config.userEnv, { + CHROME_CRASHPAD_PIPE_NAME: process.env['CHROME_CRASHPAD_PIPE_NAME'] + }); + } + // If this is the first time the window is loaded, we associate the paths // directly with the window because we assume the loading will just work if (this._readyState === ReadyState.NONE) { @@ -728,7 +750,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { } // Load URL - perf.mark('main:loadWindow'); + mark('code/willOpenNewWindow'); this._win.loadURL(this.getUrl(configuration)); // Make window visible if it did not open in N seconds because this indicates an error @@ -747,11 +769,14 @@ export class CodeWindow extends Disposable implements ICodeWindow { this._onLoad.fire(); } - reload(cli?: NativeParsedArgs): void { + async reload(cli?: NativeParsedArgs): Promise { // Copy our current config for reuse const configuration = Object.assign({}, this.currentConfig); + // Validate workspace + configuration.workspace = await this.validateWorkspace(configuration); + // Delete some properties we do not want during reload delete configuration.filesToOpenOrCreate; delete configuration.filesToDiff; @@ -761,17 +786,44 @@ export class CodeWindow extends Disposable implements ICodeWindow { // in extension development mode. These options are all development related. if (this.isExtensionDevelopmentHost && cli) { configuration.verbose = cli.verbose; + configuration.debugId = cli.debugId; configuration['inspect-extensions'] = cli['inspect-extensions']; configuration['inspect-brk-extensions'] = cli['inspect-brk-extensions']; - configuration.debugId = cli.debugId; configuration['extensions-dir'] = cli['extensions-dir']; } configuration.isInitialStartup = false; // since this is a reload // Load config - const disableExtensions = cli ? cli['disable-extensions'] : undefined; - this.load(configuration, true, disableExtensions); + this.load(configuration, { isReload: true, disableExtensions: cli?.['disable-extensions'] }); + } + + private async validateWorkspace(configuration: INativeWindowConfiguration): Promise { + + // Multi folder + if (isWorkspaceIdentifier(configuration.workspace)) { + const configPath = configuration.workspace.configPath; + if (configPath.scheme === Schemas.file) { + const workspaceExists = await this.fileService.exists(configPath); + if (!workspaceExists) { + return undefined; + } + } + } + + // Single folder + else if (isSingleFolderWorkspaceIdentifier(configuration.workspace)) { + const uri = configuration.workspace.uri; + if (uri.scheme === Schemas.file) { + const folderExists = await this.fileService.exists(uri); + if (!folderExists) { + return undefined; + } + } + } + + // Workspace is valid + return configuration.workspace; } private getUrl(windowConfiguration: INativeWindowConfiguration): string { @@ -780,6 +832,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { windowConfiguration.windowId = this._win.id; windowConfiguration.sessionId = `window:${this._win.id}`; windowConfiguration.logLevel = this.logService.getLevel(); + windowConfiguration.logsPath = this.environmentService.logsPath; // Set zoomlevel const windowConfig = this.configurationService.getValue('window'); @@ -803,14 +856,14 @@ export class CodeWindow extends Disposable implements ICodeWindow { windowConfiguration.maximized = this._win.isMaximized(); // Dump Perf Counters - windowConfiguration.perfEntries = perf.exportEntries(); + windowConfiguration.perfMarks = getMarks(); // Parts splash - windowConfiguration.partsSplashPath = path.join(this.environmentService.userDataPath, 'rapid_render.json'); + windowConfiguration.partsSplashPath = join(this.environmentService.userDataPath, 'rapid_render.json'); // OS Info windowConfiguration.os = { - release: os.release() + release: release() }; // Config (combination of process.argv and window configuration) @@ -848,10 +901,10 @@ export class CodeWindow extends Disposable implements ICodeWindow { workbench = 'vs/code/electron-browser/workbench/workbench.html'; } - return (this.environmentService.sandbox ? - FileAccess._asCodeFileUri(workbench, require) : - FileAccess.asBrowserUri(workbench, require)) - .with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` }).toString(true); + return FileAccess + .asBrowserUri(workbench, require) + .with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` }) + .toString(true); } serializeWindowState(): IWindowState { @@ -1161,7 +1214,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { if (visibility === 'toggle') { if (notify) { - this.send('vscode:showInfoMessage', nls.localize('hiddenMenuBar', "You can still access the menu bar by pressing the Alt-key.")); + this.send('vscode:showInfoMessage', localize('hiddenMenuBar', "You can still access the menu bar by pressing the Alt-key.")); } } @@ -1256,6 +1309,11 @@ export class CodeWindow extends Disposable implements ICodeWindow { send(channel: string, ...args: any[]): void { if (this._win) { + if (this._win.isDestroyed() || this._win.webContents.isDestroyed()) { + this.logService.warn(`Sending IPC message to channel ${channel} for window that is destroyed`); + return; + } + this._win.webContents.send(channel, ...args); } } diff --git a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts index 252260a5..6d339e85 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts @@ -7,11 +7,10 @@ import 'vs/css!./media/issueReporter'; import 'vs/base/browser/ui/codicons/codiconStyles'; // make sure codicon css is loaded import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { NativeHostService } from 'vs/platform/native/electron-sandbox/nativeHostService'; -import { ipcRenderer, process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { applyZoom, zoomIn, zoomOut } from 'vs/platform/windows/electron-sandbox/window'; import { $, reset, safeInnerHtml, windowOpenNoOpener } from 'vs/base/browser/dom'; import { Button } from 'vs/base/browser/ui/button/button'; -import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel'; import * as collections from 'vs/base/common/collections'; import { debounce } from 'vs/base/common/decorators'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -23,9 +22,11 @@ import BaseHtml from 'vs/code/electron-sandbox/issue/issueReporterPage'; import { localize } from 'vs/nls'; import { isRemoteDiagnosticError, SystemInfo } from 'vs/platform/diagnostics/common/diagnostics'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IMainProcessService, MainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; +import { IMainProcessService, ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { IssueReporterData, IssueReporterExtensionData, IssueReporterFeatures, IssueReporterStyles, IssueType } from 'vs/platform/issue/common/issue'; import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; +import { Codicon } from 'vs/base/common/codicons'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; const MAX_URL_LENGTH = 2045; @@ -82,14 +83,12 @@ export class IssueReporter extends Disposable { this.initServices(configuration); - const isSnap = process.platform === 'linux' && process.env.SNAP && process.env.SNAP_REVISION; - const targetExtension = configuration.data.extensionId ? configuration.data.enabledExtensions.find(extension => extension.id === configuration.data.extensionId) : undefined; this.issueReporterModel = new IssueReporterModel({ issueType: configuration.data.issueType || IssueType.Bug, versionInfo: { vscodeVersion: `${configuration.product.nameShort} ${configuration.product.version} (${configuration.product.commit || 'Commit unknown'}, ${configuration.product.date || 'Date unknown'})`, - os: `${this.configuration.os.type} ${this.configuration.os.arch} ${this.configuration.os.release}${isSnap ? ' snap' : ''}` + os: `${this.configuration.os.type} ${this.configuration.os.arch} ${this.configuration.os.release}${platform.isLinuxSnap ? ' snap' : ''}` }, extensionsDisabled: !!configuration.disableExtensions, fileOnExtension: configuration.data.extensionId ? !targetExtension?.isBuiltin : undefined, @@ -267,7 +266,7 @@ export class IssueReporter extends Disposable { private initServices(configuration: IssueReporterConfiguration): void { const serviceCollection = new ServiceCollection(); - const mainProcessService = new MainProcessService(configuration.windowId); + const mainProcessService = new ElectronIPCMainProcessService(configuration.windowId); serviceCollection.set(IMainProcessService, mainProcessService); this.nativeHostService = new NativeHostService(configuration.windowId, mainProcessService) as INativeHostService; @@ -442,7 +441,11 @@ export class IssueReporter extends Disposable { private updatePreviewButtonState() { if (this.isPreviewEnabled()) { - this.previewButton.label = localize('previewOnGitHub', "Preview on GitHub"); + if (this.configuration.data.githubAccessToken) { + this.previewButton.label = localize('createOnGitHub', "Create on GitHub"); + } else { + this.previewButton.label = localize('previewOnGitHub', "Preview on GitHub"); + } this.previewButton.enabled = true; } else { this.previewButton.enabled = false; @@ -598,8 +601,7 @@ export class IssueReporter extends Disposable { issueState = $('span.issue-state'); const issueIcon = $('span.issue-icon'); - const codicon = new CodiconLabel(issueIcon); - codicon.text = issue.state === 'open' ? '$(issue-opened)' : '$(issue-closed)'; + issueIcon.appendChild(renderIcon(issue.state === 'open' ? Codicon.issueOpened : Codicon.issueClosed)); const issueStateLabel = $('span.issue-state.label'); issueStateLabel.textContent = issue.state === 'open' ? localize('open', "Open") : localize('closed', "Closed"); @@ -778,6 +780,35 @@ export class IssueReporter extends Disposable { return isValid; } + private async submitToGitHub(issueTitle: string, issueBody: string, gitHubDetails: { owner: string, repositoryName: string }): Promise { + const url = `https://api.github.com/repos/${gitHubDetails.owner}/${gitHubDetails.repositoryName}/issues`; + const init = { + method: 'POST', + body: JSON.stringify({ + title: issueTitle, + body: issueBody + }), + headers: new Headers({ + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.configuration.data.githubAccessToken}` + }) + }; + + return new Promise((resolve, reject) => { + window.fetch(url, init).then((response) => { + if (response.ok) { + response.json().then(result => { + ipcRenderer.send('vscode:openExternal', result.html_url); + ipcRenderer.send('vscode:closeIssueReporter'); + resolve(true); + }); + } else { + resolve(false); + } + }); + }); + } + private async createIssue(): Promise { if (!this.validateInputs()) { // If inputs are invalid, set focus to the first one and add listeners on them @@ -810,8 +841,16 @@ export class IssueReporter extends Disposable { this.hasBeenSubmitted = true; - const baseUrl = this.getIssueUrlWithTitle((this.getElementById('issue-title')).value); + const issueTitle = (this.getElementById('issue-title')).value; const issueBody = this.issueReporterModel.serialize(); + + const issueUrl = this.issueReporterModel.fileOnExtension() ? this.getExtensionGitHubUrl() : this.configuration.product.reportIssueUrl!; + const gitHubDetails = this.parseGitHubUrl(issueUrl); + if (this.configuration.data.githubAccessToken && gitHubDetails) { + return this.submitToGitHub(issueTitle, issueBody, gitHubDetails); + } + + const baseUrl = this.getIssueUrlWithTitle((this.getElementById('issue-title')).value); let url = baseUrl + `&body=${encodeURIComponent(issueBody)}`; if (url.length > MAX_URL_LENGTH) { @@ -841,6 +880,20 @@ export class IssueReporter extends Disposable { }); } + private parseGitHubUrl(url: string): undefined | { repositoryName: string, owner: string } { + // Assumes a GitHub url to a particular repo, https://github.com/repositoryName/owner. + // Repository name and owner cannot contain '/' + const match = /^https?:\/\/github\.com\/([^\/]*)\/([^\/]*).*/.exec(url); + if (match && match.length) { + return { + owner: match[1], + repositoryName: match[2] + }; + } + + return undefined; + } + private getExtensionGitHubUrl(): string { let repositoryUrl = ''; const bugsUrl = this.getExtensionBugsUrl(); diff --git a/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css b/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css index add9cdae..095663c0 100644 --- a/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css +++ b/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css @@ -49,28 +49,12 @@ body { width: 90px; } -.process-item { - line-height: 22px; +.monaco-list-row:first-of-type { + border-bottom: 1px solid; } -table { - border-collapse: collapse; - width: 100%; - table-layout: fixed; -} - -th[scope='col'] { - vertical-align: bottom; - border-bottom: 1px solid #cccccc; - padding: .5rem; - border-top: 1px solid #cccccc; - cursor: default; -} - -td { - padding: .25rem; - vertical-align: top; - cursor: default; +.row { + display: flex; } .centered { @@ -79,6 +63,9 @@ td { .nameLabel{ text-align: left; + width: calc(100% - 185px); + overflow: hidden; + text-overflow: ellipsis; } .data { @@ -93,15 +80,3 @@ td { padding-left: 20px; white-space: nowrap; } - -tbody > tr:hover { - background-color: #2A2D2E; -} - -.hidden { - display: none; -} - -.header { - display: flex; -} diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorer.html b/src/vs/code/electron-sandbox/processExplorer/processExplorer.html index 517805ab..73d89eac 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorer.html +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorer.html @@ -6,7 +6,7 @@ -
    +
    diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts b/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts index ef93a1c5..e4e7be12 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts @@ -14,38 +14,226 @@ import { applyZoom, zoomIn, zoomOut } from 'vs/platform/windows/electron-sandbox import { IContextMenuItem } from 'vs/base/parts/contextmenu/common/contextmenu'; import { popup } from 'vs/base/parts/contextmenu/electron-sandbox/contextmenu'; import { ProcessItem } from 'vs/base/common/processes'; -import { addDisposableListener, $ } from 'vs/base/browser/dom'; +import * as dom from 'vs/base/browser/dom'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { isRemoteDiagnosticError, IRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; -import { MainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; -import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel'; +import { ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { ByteSize } from 'vs/platform/files/common/files'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { DataTree } from 'vs/base/browser/ui/tree/dataTree'; const DEBUG_FLAGS_PATTERN = /\s--(inspect|debug)(-brk|port)?=(\d+)?/; const DEBUG_PORT_PATTERN = /\s--(inspect|debug)-port=(\d+)/; -interface FormattedProcessItem { - cpu: number; - memory: number; - pid: string; +class ProcessListDelegate implements IListVirtualDelegate { + getHeight(element: MachineProcessInformation | ProcessItem | IRemoteDiagnosticError) { + return 22; + } + + getTemplateId(element: ProcessInformation | MachineProcessInformation | ProcessItem | IRemoteDiagnosticError) { + if (isProcessItem(element)) { + return 'process'; + } + + if (isMachineProcessInformation(element)) { + return 'machine'; + } + + if (isRemoteDiagnosticError(element)) { + return 'error'; + } + + if (isProcessInformation(element)) { + return 'header'; + } + + return ''; + } +} + +interface IProcessItemTemplateData extends IProcessRowTemplateData { + CPU: HTMLElement; + memory: HTMLElement; + PID: HTMLElement; +} + +interface IProcessRowTemplateData { + name: HTMLElement; +} + +class ProcessTreeDataSource implements IDataSource { + hasChildren(element: ProcessTree | ProcessInformation | MachineProcessInformation | ProcessItem | IRemoteDiagnosticError): boolean { + if (isRemoteDiagnosticError(element)) { + return false; + } + + if (isProcessItem(element)) { + return !!element.children?.length; + } else { + return true; + } + } + + getChildren(element: ProcessTree | ProcessInformation | MachineProcessInformation | ProcessItem | IRemoteDiagnosticError) { + if (isProcessItem(element)) { + return element.children ? element.children : []; + } + + if (isRemoteDiagnosticError(element)) { + return []; + } + + if (isProcessInformation(element)) { + // If there are multiple process roots, return these, otherwise go directly to the root process + if (element.processRoots.length > 1) { + return element.processRoots; + } else { + return [element.processRoots[0].rootProcess]; + } + } + + if (isMachineProcessInformation(element)) { + return [element.rootProcess]; + } + + return [element.processes]; + } +} + +class ProcessHeaderTreeRenderer implements ITreeRenderer { + templateId: string = 'header'; + renderTemplate(container: HTMLElement): IProcessItemTemplateData { + const data = Object.create(null); + const row = dom.append(container, dom.$('.row')); + data.name = dom.append(row, dom.$('.nameLabel')); + data.CPU = dom.append(row, dom.$('.cpu')); + data.memory = dom.append(row, dom.$('.memory')); + data.PID = dom.append(row, dom.$('.pid')); + return data; + } + renderElement(node: ITreeNode, index: number, templateData: IProcessItemTemplateData, height: number | undefined): void { + templateData.name.textContent = localize('name', "Process Name"); + templateData.CPU.textContent = localize('cpu', "CPU %"); + templateData.PID.textContent = localize('pid', "PID"); + templateData.memory.textContent = localize('memory', "Memory (MB)"); + + } + disposeTemplate(templateData: any): void { + // Nothing to do + } +} + +class MachineRenderer implements ITreeRenderer { + templateId: string = 'machine'; + renderTemplate(container: HTMLElement): IProcessRowTemplateData { + const data = Object.create(null); + const row = dom.append(container, dom.$('.row')); + data.name = dom.append(row, dom.$('.nameLabel')); + return data; + } + renderElement(node: ITreeNode, index: number, templateData: IProcessRowTemplateData, height: number | undefined): void { + templateData.name.textContent = node.element.name; + } + disposeTemplate(templateData: IProcessRowTemplateData): void { + // Nothing to do + } +} + +class ErrorRenderer implements ITreeRenderer { + templateId: string = 'error'; + renderTemplate(container: HTMLElement): IProcessRowTemplateData { + const data = Object.create(null); + const row = dom.append(container, dom.$('.row')); + data.name = dom.append(row, dom.$('.nameLabel')); + return data; + } + renderElement(node: ITreeNode, index: number, templateData: IProcessRowTemplateData, height: number | undefined): void { + templateData.name.textContent = node.element.errorMessage; + } + disposeTemplate(templateData: IProcessRowTemplateData): void { + // Nothing to do + } +} + + +class ProcessRenderer implements ITreeRenderer { + constructor(private platform: string, private totalMem: number, private mapPidToWindowTitle: Map) { } + + templateId: string = 'process'; + renderTemplate(container: HTMLElement): IProcessItemTemplateData { + const data = Object.create(null); + const row = dom.append(container, dom.$('.row')); + + data.name = dom.append(row, dom.$('.nameLabel')); + data.CPU = dom.append(row, dom.$('.cpu')); + data.memory = dom.append(row, dom.$('.memory')); + data.PID = dom.append(row, dom.$('.pid')); + + return data; + } + renderElement(node: ITreeNode, index: number, templateData: IProcessItemTemplateData, height: number | undefined): void { + const { element } = node; + + let name = element.name; + if (name === 'window') { + const windowTitle = this.mapPidToWindowTitle.get(element.pid); + name = windowTitle !== undefined ? `${name} (${this.mapPidToWindowTitle.get(element.pid)})` : name; + } + + templateData.name.textContent = name; + templateData.name.title = element.cmd; + + templateData.CPU.textContent = element.load.toFixed(0); + templateData.PID.textContent = element.pid.toFixed(0); + + const memory = this.platform === 'win32' ? element.mem : (this.totalMem * (element.mem / 100)); + templateData.memory.textContent = (memory / ByteSize.MB).toFixed(0); + } + + disposeTemplate(templateData: IProcessItemTemplateData): void { + // Nothing to do + } +} + +interface MachineProcessInformation { name: string; - formattedName: string; - cmd: string; + rootProcess: ProcessItem | IRemoteDiagnosticError +} + +interface ProcessInformation { + processRoots: MachineProcessInformation[]; +} + +interface ProcessTree { + processes: ProcessInformation; +} + +function isMachineProcessInformation(item: any): item is MachineProcessInformation { + return !!item.name && !!item.rootProcess; +} + +function isProcessInformation(item: any): item is ProcessInformation { + return !!item.processRoots; +} + +function isProcessItem(item: any): item is ProcessItem { + return !!item.pid; } class ProcessExplorer { private lastRequestTime: number; - private collapsedStateCache: Map = new Map(); - private mapPidToWindowTitle = new Map(); private listeners = new DisposableStore(); private nativeHostService: INativeHostService; + private tree: DataTree | undefined; + constructor(windowId: number, private data: ProcessExplorerData) { - const mainProcessService = new MainProcessService(windowId); + const mainProcessService = new ElectronIPCMainProcessService(windowId); this.nativeHostService = new NativeHostService(windowId, mainProcessService) as INativeHostService; this.applyStyles(data.styles); @@ -56,8 +244,19 @@ class ProcessExplorer { windows.forEach(window => this.mapPidToWindowTitle.set(window.pid, window.title)); }); - ipcRenderer.on('vscode:listProcessesResponse', (event: unknown, processRoots: [{ name: string, rootProcess: ProcessItem | IRemoteDiagnosticError }]) => { - this.updateProcessInfo(processRoots); + ipcRenderer.on('vscode:listProcessesResponse', async (event: unknown, processRoots: MachineProcessInformation[]) => { + processRoots.forEach((info, index) => { + if (isProcessItem(info.rootProcess)) { + info.rootProcess.name = index === 0 ? `${this.data.applicationName} main` : 'remote agent'; + } + }); + + if (!this.tree) { + await this.createProcessTree(processRoots); + } else { + this.tree.setInput({ processes: { processRoots } }); + } + this.requestProcessList(0); }); @@ -66,54 +265,58 @@ class ProcessExplorer { ipcRenderer.send('vscode:listProcesses'); } - private getProcessList(rootProcess: ProcessItem, isLocal: boolean, totalMem: number): FormattedProcessItem[] { - const processes: FormattedProcessItem[] = []; - const handledProcesses = new Set(); - - if (rootProcess) { - this.getProcessItem(processes, rootProcess, 0, isLocal, totalMem, handledProcesses); + private async createProcessTree(processRoots: MachineProcessInformation[]): Promise { + const container = document.getElementById('process-list'); + if (!container) { + return; } - return processes; - } + const { totalmem } = await this.nativeHostService.getOSStatistics(); - private getProcessItem(processes: FormattedProcessItem[], item: ProcessItem, indent: number, isLocal: boolean, totalMem: number, handledProcesses: Set): void { - const isRoot = (indent === 0); + const renderers = [ + new ProcessRenderer(this.data.platform, totalmem, this.mapPidToWindowTitle), + new ProcessHeaderTreeRenderer(), + new MachineRenderer(), + new ErrorRenderer() + ]; - handledProcesses.add(item.pid); + this.tree = new DataTree('processExplorer', + container, + new ProcessListDelegate(), + renderers, + new ProcessTreeDataSource(), + { + identityProvider: + { + getId: (element: ProcessTree | ProcessItem | MachineProcessInformation | ProcessInformation | IRemoteDiagnosticError) => { + if (isProcessItem(element)) { + return element.pid.toString(); + } - let name = item.name; - if (isRoot) { - name = isLocal ? `${this.data.applicationName} main` : 'remote agent'; - } + if (isRemoteDiagnosticError(element)) { + return element.hostName; + } - if (name === 'window') { - const windowTitle = this.mapPidToWindowTitle.get(item.pid); - name = windowTitle !== undefined ? `${name} (${this.mapPidToWindowTitle.get(item.pid)})` : name; - } + if (isProcessInformation(element)) { + return 'processes'; + } - // Format name with indent - const formattedName = isRoot ? name : `${' '.repeat(indent)} ${name}`; - const memory = this.data.platform === 'win32' ? item.mem : (totalMem * (item.mem / 100)); - processes.push({ - cpu: item.load, - memory: (memory / ByteSize.MB), - pid: item.pid.toFixed(0), - name, - formattedName, - cmd: item.cmd - }); + if (isMachineProcessInformation(element)) { + return element.name; + } - // Recurse into children if any - if (Array.isArray(item.children)) { - item.children.forEach(child => { - if (!child || handledProcesses.has(child.pid)) { - return; // prevent loops + return 'header'; + } } - - this.getProcessItem(processes, child, indent + 1, isLocal, totalMem, handledProcesses); }); - } + + this.tree.setInput({ processes: { processRoots } }); + this.tree.layout(window.innerHeight, window.innerWidth); + this.tree.onContextMenu(e => { + if (isProcessItem(e.element)) { + this.showContextMenu(e.element, true); + } + }); } private isDebuggable(cmd: string): boolean { @@ -121,7 +324,7 @@ class ProcessExplorer { return (matches && matches.length >= 2) || cmd.indexOf('node ') >= 0 || cmd.indexOf('node.exe') >= 0; } - private attachTo(item: FormattedProcessItem) { + private attachTo(item: ProcessItem) { const config: any = { type: 'node', request: 'attach', @@ -150,179 +353,16 @@ class ProcessExplorer { ipcRenderer.send('vscode:workbenchCommand', { id: 'debug.startFromConfig', from: 'processExplorer', args: [config] }); } - private getProcessIdWithHighestProperty(processList: any[], propertyName: string) { - let max = 0; - let maxProcessId; - processList.forEach(process => { - if (process[propertyName] > max) { - max = process[propertyName]; - maxProcessId = process.pid; - } - }); - - return maxProcessId; - } - - private updateSectionCollapsedState(shouldExpand: boolean, body: HTMLElement, twistie: CodiconLabel, sectionName: string) { - if (shouldExpand) { - body.classList.remove('hidden'); - this.collapsedStateCache.set(sectionName, false); - twistie.text = '$(chevron-down)'; - } else { - body.classList.add('hidden'); - this.collapsedStateCache.set(sectionName, true); - twistie.text = '$(chevron-right)'; - } - } - - private renderProcessFetchError(sectionName: string, errorMessage: string) { - const container = document.getElementById('process-list'); - if (!container) { - return; - } - - const body = document.createElement('tbody'); - - this.renderProcessGroupHeader(sectionName, body, container); - - const errorRow = document.createElement('tr'); - const data = document.createElement('td'); - data.textContent = errorMessage; - data.className = 'error'; - data.colSpan = 4; - errorRow.appendChild(data); - - body.appendChild(errorRow); - container.appendChild(body); - } - - private renderProcessGroupHeader(sectionName: string, body: HTMLElement, container: HTMLElement) { - const headerRow = document.createElement('tr'); - - const headerData = document.createElement('td'); - headerData.colSpan = 4; - headerRow.appendChild(headerData); - - const headerContainer = document.createElement('div'); - headerContainer.className = 'header'; - headerData.appendChild(headerContainer); - - const twistieContainer = document.createElement('div'); - const twistieCodicon = new CodiconLabel(twistieContainer); - this.updateSectionCollapsedState(!this.collapsedStateCache.get(sectionName), body, twistieCodicon, sectionName); - headerContainer.appendChild(twistieContainer); - - const headerLabel = document.createElement('span'); - headerLabel.textContent = sectionName; - headerContainer.appendChild(headerLabel); - - this.listeners.add(addDisposableListener(headerData, 'click', (e) => { - const isHidden = body.classList.contains('hidden'); - this.updateSectionCollapsedState(isHidden, body, twistieCodicon, sectionName); - })); - - container.appendChild(headerRow); - } - - private renderTableSection(sectionName: string, processList: FormattedProcessItem[], renderManySections: boolean, sectionIsLocal: boolean): void { - const container = document.getElementById('process-list'); - if (!container) { - return; - } - - const highestCPUProcess = this.getProcessIdWithHighestProperty(processList, 'cpu'); - const highestMemoryProcess = this.getProcessIdWithHighestProperty(processList, 'memory'); - - const body = document.createElement('tbody'); - - if (renderManySections) { - this.renderProcessGroupHeader(sectionName, body, container); - } - - processList.forEach(p => { - const row = document.createElement('tr'); - row.id = p.pid.toString(); - - const cpu = document.createElement('td'); - p.pid === highestCPUProcess - ? cpu.classList.add('centered', 'highest') - : cpu.classList.add('centered'); - cpu.textContent = p.cpu.toFixed(0); - - const memory = document.createElement('td'); - p.pid === highestMemoryProcess - ? memory.classList.add('centered', 'highest') - : memory.classList.add('centered'); - memory.textContent = p.memory.toFixed(0); - - const pid = document.createElement('td'); - pid.classList.add('centered'); - pid.textContent = p.pid; - - const name = document.createElement('th'); - name.scope = 'row'; - name.classList.add('data'); - name.title = p.cmd; - name.textContent = p.formattedName; - - row.append(cpu, memory, pid, name); - - this.listeners.add(addDisposableListener(row, 'contextmenu', (e) => { - this.showContextMenu(e, p, sectionIsLocal); - })); - - body.appendChild(row); - }); - - container.appendChild(body); - } - - private async updateProcessInfo(processLists: [{ name: string, rootProcess: ProcessItem | IRemoteDiagnosticError }]): Promise { - const container = document.getElementById('process-list'); - if (!container) { - return; - } - - container.innerText = ''; - this.listeners.clear(); - - const tableHead = $('thead', undefined); - const row = $('tr'); - tableHead.append(row); - - row.append($('th.cpu', { scope: 'col' }, localize('cpu', "CPU %"))); - row.append($('th.memory', { scope: 'col' }, localize('memory', "Memory (MB)"))); - row.append($('th.pid', { scope: 'col' }, localize('pid', "PID"))); - row.append($('th.nameLabel', { scope: 'col' }, localize('name', "Name"))); - - container.append(tableHead); - - const hasMultipleMachines = Object.keys(processLists).length > 1; - const { totalmem } = await this.nativeHostService.getOSStatistics(); - processLists.forEach((remote, i) => { - const isLocal = i === 0; - if (isRemoteDiagnosticError(remote.rootProcess)) { - this.renderProcessFetchError(remote.name, remote.rootProcess.errorMessage); - } else { - this.renderTableSection(remote.name, this.getProcessList(remote.rootProcess, isLocal, totalmem), hasMultipleMachines, isLocal); - } - }); - } - private applyStyles(styles: ProcessExplorerStyles): void { const styleTag = document.createElement('style'); const content: string[] = []; if (styles.hoverBackground) { - content.push(`tbody > tr:hover, table > tr:hover { background-color: ${styles.hoverBackground}; }`); + content.push(`.monaco-list-row:hover { background-color: ${styles.hoverBackground}; }`); } if (styles.hoverForeground) { - content.push(`tbody > tr:hover, table > tr:hover { color: ${styles.hoverForeground}; }`); - } - - if (styles.highlightForeground) { - content.push(`.highest { color: ${styles.highlightForeground}; }`); + content.push(`.monaco-list-row:hover { color: ${styles.hoverForeground}; }`); } styleTag.textContent = content.join('\n'); @@ -334,9 +374,7 @@ class ProcessExplorer { } } - private showContextMenu(e: MouseEvent, item: FormattedProcessItem, isLocal: boolean) { - e.preventDefault(); - + private showContextMenu(item: ProcessItem, isLocal: boolean) { const items: IContextMenuItem[] = []; const pid = Number(item.pid); @@ -417,8 +455,6 @@ class ProcessExplorer { } } - - export function startup(windowId: number, data: ProcessExplorerData): void { const platformClass = data.platform === 'win32' ? 'windows' : data.platform === 'linux' ? 'linux' : 'mac'; document.body.classList.add(platformClass); // used by our fonts diff --git a/src/vs/code/electron-sandbox/proxy/auth.html b/src/vs/code/electron-sandbox/proxy/auth.html deleted file mode 100644 index 788b68fc..00000000 --- a/src/vs/code/electron-sandbox/proxy/auth.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - -

    -
    -

    -
    -

    -

    -

    - - -

    -
    -
    - - - - - diff --git a/src/vs/code/electron-sandbox/proxy/auth.js b/src/vs/code/electron-sandbox/proxy/auth.js deleted file mode 100644 index 5e0db3c2..00000000 --- a/src/vs/code/electron-sandbox/proxy/auth.js +++ /dev/null @@ -1,48 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -const { ipcRenderer } = window.vscode; - -function promptForCredentials(data) { - return new Promise((c, e) => { - const $title = document.getElementById('title'); - const $username = document.getElementById('username'); - const $password = document.getElementById('password'); - const $form = document.getElementById('form'); - const $cancel = document.getElementById('cancel'); - const $message = document.getElementById('message'); - - function submit() { - c({ username: $username.value, password: $password.value }); - return false; - } - - function cancel() { - c({ username: '', password: '' }); - return false; - } - - $form.addEventListener('submit', submit); - $cancel.addEventListener('click', cancel); - - document.body.addEventListener('keydown', function (e) { - switch (e.keyCode) { - case 27: e.preventDefault(); e.stopPropagation(); return cancel(); - case 13: e.preventDefault(); e.stopPropagation(); return submit(); - } - }); - - $title.textContent = data.title; - $message.textContent = data.message; - $username.focus(); - }); -} - -ipcRenderer.on('vscode:openProxyAuthDialog', async (event, data) => { - const response = await promptForCredentials(data); - ipcRenderer.send('vscode:proxyAuthResponse', response); -}); diff --git a/src/vs/code/electron-sandbox/workbench/workbench.html b/src/vs/code/electron-sandbox/workbench/workbench.html index 40737461..f36737f2 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.html +++ b/src/vs/code/electron-sandbox/workbench/workbench.html @@ -3,7 +3,8 @@ - + + diff --git a/src/vs/code/electron-sandbox/workbench/workbench.js b/src/vs/code/electron-sandbox/workbench/workbench.js index 6a46ef2b..c5cb1deb 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.js +++ b/src/vs/code/electron-sandbox/workbench/workbench.js @@ -12,8 +12,7 @@ const bootstrapWindow = bootstrapWindowLib(); // Add a perf entry right from the top - const perf = bootstrapWindow.perfLib(); - perf.mark('renderer/started'); + performance.mark('code/didStartRenderer'); // Load workbench main JS, CSS and NLS all in parallel. This is an // optimization to prevent a waterfall of loading to happen, because @@ -24,10 +23,10 @@ 'vs/nls!vs/workbench/workbench.desktop.main', 'vs/css!vs/workbench/workbench.desktop.main' ], - async function (workbench, configuration) { + function (_, configuration) { // Mark start of workbench - perf.mark('didLoadWorkbenchMain'); + performance.mark('code/didLoadWorkbenchMain'); // @ts-ignore return require('vs/workbench/electron-sandbox/desktop.main').main(configuration); @@ -41,19 +40,34 @@ loaderConfig.recordStats = true; }, beforeRequire: function () { - perf.mark('willLoadWorkbenchMain'); + performance.mark('code/willLoadWorkbenchMain'); } } ); + // add default trustedTypes-policy for logging and to workaround + // lib/platform limitations + window.trustedTypes?.createPolicy('default', { + createHTML(value) { + // see https://github.com/electron/electron/issues/27211 + // Electron webviews use a static innerHTML default value and + // that isn't trusted. We use a default policy to check for the + // exact value of that innerHTML-string and only allow that. + if (value === '') { + return value; + } + throw new Error('UNTRUSTED html usage, default trusted types policy should NEVER be reached'); + // console.trace('UNTRUSTED html usage, default trusted types policy should NEVER be reached'); + // return value; + } + }); //region Helpers /** * @returns {{ - * load: (modules: string[], resultCallback: (result, configuration: object) => any, options: object) => unknown, - * globals: () => typeof import('../../../base/parts/sandbox/electron-sandbox/globals'), - * perfLib: () => { mark: (name: string) => void } + * load: (modules: string[], resultCallback: (result, configuration: import('../../../platform/windows/common/windows').INativeWindowConfiguration) => any, options: object) => unknown, + * globals: () => typeof import('../../../base/parts/sandbox/electron-sandbox/globals') * }} */ function bootstrapWindowLib() { diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index c6415a55..ac557018 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as os from 'os'; -import * as fs from 'fs'; +import { homedir } from 'os'; +import { constants, existsSync, statSync, unlinkSync, chmodSync, truncateSync, readFileSync } from 'fs'; import { spawn, ChildProcess, SpawnOptions } from 'child_process'; import { buildHelpMessage, buildVersionMessage, OPTIONS } from 'vs/platform/environment/node/argv'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { parseCLIProcessArgv, addArg } from 'vs/platform/environment/node/argvHelper'; import { createWaitMarkerFile } from 'vs/platform/environment/node/waitMarkerFile'; import product from 'vs/platform/product/common/product'; -import * as paths from 'vs/base/common/path'; +import { isAbsolute, join } from 'vs/base/common/path'; import { whenDeleted, writeFileSync } from 'vs/base/node/pfs'; import { findFreePort, randomPort } from 'vs/base/node/ports'; import { isWindows, isLinux } from 'vs/base/common/platform'; @@ -69,10 +69,10 @@ export async function main(argv: string[]): Promise { // Validate if ( - !source || !target || source === target || // make sure source and target are provided and are not the same - !paths.isAbsolute(source) || !paths.isAbsolute(target) || // make sure both source and target are absolute paths - !fs.existsSync(source) || !fs.statSync(source).isFile() || // make sure source exists as file - !fs.existsSync(target) || !fs.statSync(target).isFile() // make sure target exists as file + !source || !target || source === target || // make sure source and target are provided and are not the same + !isAbsolute(source) || !isAbsolute(target) || // make sure both source and target are absolute paths + !existsSync(source) || !statSync(source).isFile() || // make sure source exists as file + !existsSync(target) || !statSync(target).isFile() // make sure target exists as file ) { throw new Error('Using --file-write with invalid arguments.'); } @@ -83,15 +83,15 @@ export async function main(argv: string[]): Promise { let targetMode: number = 0; let restoreMode = false; if (!!args['file-chmod']) { - targetMode = fs.statSync(target).mode; - if (!(targetMode & 128) /* readonly */) { - fs.chmodSync(target, targetMode | 128); + targetMode = statSync(target).mode; + if (!(targetMode & constants.S_IWUSR)) { + chmodSync(target, targetMode | constants.S_IWUSR); restoreMode = true; } } // Write source to target - const data = fs.readFileSync(source); + const data = readFileSync(source); if (isWindows) { // On Windows we use a different strategy of saving the file // by first truncating the file and then writing with r+ mode. @@ -99,7 +99,7 @@ export async function main(argv: string[]): Promise { // (see https://github.com/microsoft/vscode/issues/931) and // prevent removing alternate data streams // (see https://github.com/microsoft/vscode/issues/6363) - fs.truncateSync(target, 0); + truncateSync(target, 0); writeFileSync(target, data, { flag: 'r+' }); } else { writeFileSync(target, data); @@ -107,7 +107,7 @@ export async function main(argv: string[]): Promise { // Restore previous mode as needed if (restoreMode) { - fs.chmodSync(target, targetMode); + chmodSync(target, targetMode); } } catch (error) { error.message = `Error using --file-write: ${error.message}`; @@ -215,7 +215,7 @@ export async function main(argv: string[]): Promise { throw new Error('Failed to find free ports for profiler. Make sure to shutdown all instances of the editor first.'); } - const filenamePrefix = paths.join(os.homedir(), 'prof-' + Math.random().toString(16).slice(-4)); + const filenamePrefix = join(homedir(), 'prof-' + Math.random().toString(16).slice(-4)); addArg(argv, `--inspect-brk=${portMain}`); addArg(argv, `--remote-debugging-port=${portRenderer}`); @@ -338,7 +338,7 @@ export async function main(argv: string[]): Promise { // Make sure to delete the tmp stdin file if we have any if (stdinFilePath) { - fs.unlinkSync(stdinFilePath); + unlinkSync(stdinFilePath); } }); } diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 84fa06d7..ee189f83 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; +import { release } from 'os'; +import * as fs from 'fs'; +import { gracefulify } from 'graceful-fs'; +import { isAbsolute, join } from 'vs/base/common/path'; import { raceTimeout } from 'vs/base/common/async'; -import * as semver from 'vs/base/common/semver/semver'; import product from 'vs/platform/product/common/product'; -import * as path from 'vs/base/common/path'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -15,416 +16,147 @@ import { InstantiationService } from 'vs/platform/instantiation/common/instantia import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; -import { IExtensionManagementService, IExtensionGalleryService, IGalleryExtension, ILocalExtension, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, IExtensionGalleryService, IExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { combinedAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; -import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; +import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties'; import { IRequestService } from 'vs/platform/request/common/request'; import { RequestService } from 'vs/platform/request/node/requestService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; import { mkdirp, writeFile } from 'vs/base/node/pfs'; -import { getBaseLabel } from 'vs/base/common/labels'; import { IStateService } from 'vs/platform/state/node/state'; import { StateService } from 'vs/platform/state/node/stateService'; import { ILogService, getLogLevel, LogLevel, ConsoleLogService, MultiplexLogService } from 'vs/platform/log/common/log'; -import { isPromiseCanceledError } from 'vs/base/common/errors'; -import { areSameExtensions, adoptToGalleryExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { URI } from 'vs/base/common/uri'; -import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; -import { IExtensionManifest, ExtensionType, isLanguagePackExtension, EXTENSION_CATEGORIES } from 'vs/platform/extensions/common/extensions'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; import { Schemas } from 'vs/base/common/network'; import { SpdLogService } from 'vs/platform/log/node/spdlogService'; import { buildTelemetryMessage } from 'vs/platform/telemetry/node/telemetry'; import { FileService } from 'vs/platform/files/common/fileService'; import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { IProductService } from 'vs/platform/product/common/productService'; +import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService'; +import { URI } from 'vs/base/common/uri'; +import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; +import { setUnexpectedErrorHandler } from 'vs/base/common/errors'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; -const notFound = (id: string) => localize('notFound', "Extension '{0}' not found.", id); -const notInstalled = (id: string) => localize('notInstalled', "Extension '{0}' is not installed.", id); -const useId = localize('useId', "Make sure you use the full extension ID, including the publisher, e.g.: {0}", 'ms-dotnettools.csharp'); - -function getId(manifest: IExtensionManifest, withVersion?: boolean): string { - if (withVersion) { - return `${manifest.publisher}.${manifest.name}@${manifest.version}`; - } else { - return `${manifest.publisher}.${manifest.name}`; - } -} - -const EXTENSION_ID_REGEX = /^([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/; - -export function getIdAndVersion(id: string): [string, string | undefined] { - const matches = EXTENSION_ID_REGEX.exec(id); - if (matches && matches[1]) { - return [adoptToGalleryExtensionId(matches[1]), matches[2]]; - } - return [adoptToGalleryExtensionId(id), undefined]; -} - -type InstallExtensionInfo = { id: string, version?: string, installOptions: InstallOptions }; - -export class Main { +class CliMain extends Disposable { constructor( - @IInstantiationService private readonly instantiationService: IInstantiationService, - @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, - ) { } + private argv: NativeParsedArgs + ) { + super(); - async run(argv: NativeParsedArgs): Promise { - if (argv['install-source']) { - await this.setInstallSource(argv['install-source']); - } else if (argv['list-extensions']) { - await this.listExtensions(!!argv['show-versions'], argv['category']); - } else if (argv['install-extension'] || argv['install-builtin-extension']) { - await this.installExtensions(argv['install-extension'] || [], argv['install-builtin-extension'] || [], !!argv['do-not-sync'], !!argv['force']); - } else if (argv['uninstall-extension']) { - await this.uninstallExtension(argv['uninstall-extension'], !!argv['force']); - } else if (argv['locate-extension']) { - await this.locateExtension(argv['locate-extension']); - } else if (argv['telemetry']) { - console.log(buildTelemetryMessage(this.environmentService.appRoot, this.environmentService.extensionsPath)); - } + // Enable gracefulFs + gracefulify(fs); + + this.registerListeners(); } - private setInstallSource(installSource: string): Promise { - return writeFile(this.environmentService.installSourcePath, installSource.slice(0, 30)); + private registerListeners(): void { + + // Dispose on exit + process.once('exit', () => this.dispose()); } - private async listExtensions(showVersions: boolean, category?: string): Promise { - let extensions = await this.extensionManagementService.getInstalled(ExtensionType.User); - const categories = EXTENSION_CATEGORIES.map(c => c.toLowerCase()); - if (category && category !== '') { - if (categories.indexOf(category.toLowerCase()) < 0) { - console.log('Invalid category please enter a valid category. To list valid categories run --category without a category specified'); - return; - } - extensions = extensions.filter(e => { - if (e.manifest.categories) { - const lowerCaseCategories: string[] = e.manifest.categories.map(c => c.toLowerCase()); - return lowerCaseCategories.indexOf(category.toLowerCase()) > -1; - } - return false; - }); - } else if (category === '') { - console.log('Possible Categories: '); - categories.forEach(category => { - console.log(category); - }); - return; - } - extensions.forEach(e => console.log(getId(e.manifest, showVersions))); - } + async run(): Promise { - async installExtensions(extensions: string[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean): Promise { - const failed: string[] = []; - const installedExtensionsManifests: IExtensionManifest[] = []; - if (extensions.length) { - console.log(localize('installingExtensions', "Installing extensions...")); - } + // Services + const [instantiationService, appenders] = await this.initServices(); - const installed = await this.extensionManagementService.getInstalled(ExtensionType.User); - const checkIfNotInstalled = (id: string, version?: string): boolean => { - const installedExtension = installed.find(i => areSameExtensions(i.identifier, { id })); - if (installedExtension) { - if (!version && !force) { - console.log(localize('alreadyInstalled-checkAndUpdate', "Extension '{0}' v{1} is already installed. Use '--force' option to update to latest version or provide '@' to install a specific version, for example: '{2}@1.2.3'.", id, installedExtension.manifest.version, id)); - return false; - } - if (version && installedExtension.manifest.version === version) { - console.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", `${id}@${version}`)); - return false; - } - } - return true; - }; - const vsixs: string[] = []; - const installExtensionInfos: InstallExtensionInfo[] = []; - for (const extension of extensions) { - if (/\.vsix$/i.test(extension)) { - vsixs.push(extension); - } else { - const [id, version] = getIdAndVersion(extension); - if (checkIfNotInstalled(id, version)) { - installExtensionInfos.push({ id, version, installOptions: { isBuiltin: false, isMachineScoped } }); - } - } - } - for (const extension of builtinExtensionIds) { - const [id, version] = getIdAndVersion(extension); - if (checkIfNotInstalled(id, version)) { - installExtensionInfos.push({ id, version, installOptions: { isBuiltin: true, isMachineScoped: false } }); - } - } + return instantiationService.invokeFunction(async accessor => { + const logService = accessor.get(ILogService); + const environmentService = accessor.get(INativeEnvironmentService); + const extensionManagementCLIService = accessor.get(IExtensionManagementCLIService); - if (vsixs.length) { - await Promise.all(vsixs.map(async vsix => { - try { - const manifest = await this.installVSIX(vsix, force); - if (manifest) { - installedExtensionsManifests.push(manifest); - } - } catch (err) { - console.error(err.message || err.stack || err); - failed.push(vsix); - } - })); - } + // Log info + logService.info('CLI main', this.argv); - if (installExtensionInfos.length) { + // Error handler + this.registerErrorHandler(logService); - const galleryExtensions = await this.getGalleryExtensions(installExtensionInfos); + // Run based on argv + await this.doRun(environmentService, extensionManagementCLIService); - await Promise.all(installExtensionInfos.map(async extensionInfo => { - const gallery = galleryExtensions.get(extensionInfo.id.toLowerCase()); - if (gallery) { - try { - const manifest = await this.installFromGallery(extensionInfo, gallery, installed, force); - if (manifest) { - installedExtensionsManifests.push(manifest); - } - } catch (err) { - console.error(err.message || err.stack || err); - failed.push(extensionInfo.id); - } - } else { - console.error(`${notFound(extensionInfo.version ? `${extensionInfo.id}@${extensionInfo.version}` : extensionInfo.id)}\n${useId}`); - failed.push(extensionInfo.id); - } - })); - - } - - if (installedExtensionsManifests.some(manifest => isLanguagePackExtension(manifest))) { - await this.updateLocalizationsCache(); - } - - if (failed.length) { - throw new Error(localize('installation failed', "Failed Installing Extensions: {0}", failed.join(', '))); - } - } - - private async installVSIX(vsix: string, force: boolean): Promise { - vsix = path.isAbsolute(vsix) ? vsix : path.join(process.cwd(), vsix); - const manifest = await getManifest(vsix); - const valid = await this.validate(manifest, force); - if (valid) { - try { - await this.extensionManagementService.install(URI.file(vsix)); - console.log(localize('successVsixInstall', "Extension '{0}' was successfully installed.", getBaseLabel(vsix))); - return manifest; - } catch (error) { - if (isPromiseCanceledError(error)) { - console.log(localize('cancelVsixInstall', "Cancelled installing extension '{0}'.", getBaseLabel(vsix))); - return null; - } else { - throw error; - } - } - } - return null; - } - - private async getGalleryExtensions(extensions: InstallExtensionInfo[]): Promise> { - const extensionIds = extensions.filter(({ version }) => version === undefined).map(({ id }) => id); - const extensionsWithIdAndVersion = extensions.filter(({ version }) => version !== undefined); - - const galleryExtensions = new Map(); - await Promise.all([ - (async () => { - const result = await this.extensionGalleryService.getExtensions(extensionIds, CancellationToken.None); - result.forEach(extension => galleryExtensions.set(extension.identifier.id.toLowerCase(), extension)); - })(), - Promise.all(extensionsWithIdAndVersion.map(async ({ id, version }) => { - const extension = await this.extensionGalleryService.getCompatibleExtension({ id }, version); - if (extension) { - galleryExtensions.set(extension.identifier.id.toLowerCase(), extension); - } - })) - ]); - - return galleryExtensions; - } - - private async installFromGallery({ id, version, installOptions }: InstallExtensionInfo, galleryExtension: IGalleryExtension, installed: ILocalExtension[], force: boolean): Promise { - const manifest = await this.extensionGalleryService.getManifest(galleryExtension, CancellationToken.None); - const installedExtension = installed.find(e => areSameExtensions(e.identifier, galleryExtension.identifier)); - if (installedExtension) { - if (galleryExtension.version === installedExtension.manifest.version) { - console.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", version ? `${id}@${version}` : id)); - return null; - } - console.log(localize('updateMessage', "Updating the extension '{0}' to the version {1}", id, galleryExtension.version)); - } - - try { - if (installOptions.isBuiltin) { - console.log(localize('installing builtin ', "Installing builtin extension '{0}' v{1}...", id, galleryExtension.version)); - } else { - console.log(localize('installing', "Installing extension '{0}' v{1}...", id, galleryExtension.version)); - } - await this.extensionManagementService.installFromGallery(galleryExtension, installOptions); - console.log(localize('successInstall', "Extension '{0}' v{1} was successfully installed.", id, galleryExtension.version)); - return manifest; - } catch (error) { - if (isPromiseCanceledError(error)) { - console.log(localize('cancelInstall', "Cancelled installing extension '{0}'.", id)); - return null; - } else { - throw error; - } - } - } - - private async validate(manifest: IExtensionManifest, force: boolean): Promise { - if (!manifest) { - throw new Error('Invalid vsix'); - } - - const extensionIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; - const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User); - const newer = installedExtensions.find(local => areSameExtensions(extensionIdentifier, local.identifier) && semver.gt(local.manifest.version, manifest.version)); - - if (newer && !force) { - console.log(localize('forceDowngrade', "A newer version of extension '{0}' v{1} is already installed. Use '--force' option to downgrade to older version.", newer.identifier.id, newer.manifest.version, manifest.version)); - return false; - } - - return true; - } - - private async uninstallExtension(extensions: string[], force: boolean): Promise { - async function getExtensionId(extensionDescription: string): Promise { - if (!/\.vsix$/i.test(extensionDescription)) { - return extensionDescription; - } - - const zipPath = path.isAbsolute(extensionDescription) ? extensionDescription : path.join(process.cwd(), extensionDescription); - const manifest = await getManifest(zipPath); - return getId(manifest); - } - - const uninstalledExtensions: ILocalExtension[] = []; - for (const extension of extensions) { - const id = await getExtensionId(extension); - const installed = await this.extensionManagementService.getInstalled(); - const extensionToUninstall = installed.find(e => areSameExtensions(e.identifier, { id })); - if (!extensionToUninstall) { - throw new Error(`${notInstalled(id)}\n${useId}`); - } - if (extensionToUninstall.type === ExtensionType.System) { - console.log(localize('builtin', "Extension '{0}' is a Built-in extension and cannot be installed", id)); - return; - } - if (extensionToUninstall.isBuiltin && !force) { - console.log(localize('forceUninstall', "Extension '{0}' is marked as a Built-in extension by user. Please use '--force' option to uninstall it.", id)); - return; - } - console.log(localize('uninstalling', "Uninstalling {0}...", id)); - await this.extensionManagementService.uninstall(extensionToUninstall); - uninstalledExtensions.push(extensionToUninstall); - console.log(localize('successUninstall', "Extension '{0}' was successfully uninstalled!", id)); - } - - if (uninstalledExtensions.some(e => isLanguagePackExtension(e.manifest))) { - await this.updateLocalizationsCache(); - } - } - - private async locateExtension(extensions: string[]): Promise { - const installed = await this.extensionManagementService.getInstalled(); - extensions.forEach(e => { - installed.forEach(i => { - if (i.identifier.id === e) { - if (i.location.scheme === Schemas.file) { - console.log(i.location.fsPath); - return; - } - } - }); + // Flush the remaining data in AI adapter (with 1s timeout) + return raceTimeout(combinedAppender(...appenders).flush(), 1000); }); } - private async updateLocalizationsCache(): Promise { - const localizationService = this.instantiationService.createInstance(LocalizationsService); - await localizationService.update(); - localizationService.dispose(); - } -} + private async initServices(): Promise<[IInstantiationService, AppInsightsAppender[]]> { + const services = new ServiceCollection(); -const eventPrefix = 'monacoworkbench'; + // Environment + const environmentService = new NativeEnvironmentService(this.argv); + services.set(IEnvironmentService, environmentService); + services.set(INativeEnvironmentService, environmentService); -export async function main(argv: NativeParsedArgs): Promise { - const services = new ServiceCollection(); - const disposables = new DisposableStore(); + // Init folders + await Promise.all([environmentService.appSettingsHome.fsPath, environmentService.extensionsPath].map(path => path ? mkdirp(path) : undefined)); - const environmentService = new NativeEnvironmentService(argv); - const logLevel = getLogLevel(environmentService); - const loggers: ILogService[] = []; - loggers.push(new SpdLogService('cli', environmentService.logsPath, logLevel)); - if (logLevel === LogLevel.Trace) { - loggers.push(new ConsoleLogService(logLevel)); - } - const logService = new MultiplexLogService(loggers); - process.once('exit', () => logService.dispose()); - logService.info('main', argv); + // Log + const logLevel = getLogLevel(environmentService); + const loggers: ILogService[] = []; + loggers.push(new SpdLogService('cli', environmentService.logsPath, logLevel)); + if (logLevel === LogLevel.Trace) { + loggers.push(new ConsoleLogService(logLevel)); + } - await Promise.all([environmentService.appSettingsHome.fsPath, environmentService.extensionsPath] - .map((path): undefined | Promise => path ? mkdirp(path) : undefined)); + const logService = this._register(new MultiplexLogService(loggers)); + services.set(ILogService, logService); - // Files - const fileService = new FileService(logService); - disposables.add(fileService); - services.set(IFileService, fileService); + // Files + const fileService = this._register(new FileService(logService)); + services.set(IFileService, fileService); - const diskFileSystemProvider = new DiskFileSystemProvider(logService); - disposables.add(diskFileSystemProvider); - fileService.registerProvider(Schemas.file, diskFileSystemProvider); + const diskFileSystemProvider = this._register(new DiskFileSystemProvider(logService)); + fileService.registerProvider(Schemas.file, diskFileSystemProvider); - const configurationService = new ConfigurationService(environmentService.settingsResource, fileService); - disposables.add(configurationService); - await configurationService.initialize(); + // Configuration + const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService)); + services.set(IConfigurationService, configurationService); - services.set(IEnvironmentService, environmentService); - services.set(INativeEnvironmentService, environmentService); + // Init config + await configurationService.initialize(); - services.set(ILogService, logService); - services.set(IConfigurationService, configurationService); - services.set(IStateService, new SyncDescriptor(StateService)); - services.set(IProductService, { _serviceBrand: undefined, ...product }); + // State + const stateService = new StateService(environmentService, logService); + services.set(IStateService, stateService); - const instantiationService: IInstantiationService = new InstantiationService(services); - - return instantiationService.invokeFunction(async accessor => { - const stateService = accessor.get(IStateService); + // Product + services.set(IProductService, { _serviceBrand: undefined, ...product }); const { appRoot, extensionsPath, extensionDevelopmentLocationURI, isBuilt, installSourcePath } = environmentService; - const services = new ServiceCollection(); + // Request services.set(IRequestService, new SyncDescriptor(RequestService)); + + // Extensions services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); + services.set(IExtensionManagementCLIService, new SyncDescriptor(ExtensionManagementCLIService)); + // Localizations + services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService)); + + // Telemetry const appenders: AppInsightsAppender[] = []; if (isBuilt && !extensionDevelopmentLocationURI && !environmentService.disableTelemetry && product.enableTelemetry) { if (product.aiConfig && product.aiConfig.asimovKey) { - appenders.push(new AppInsightsAppender(eventPrefix, null, product.aiConfig.asimovKey)); + appenders.push(new AppInsightsAppender('monacoworkbench', null, product.aiConfig.asimovKey)); } const config: ITelemetryServiceConfig = { appender: combinedAppender(...appenders), sendErrorTelemetry: false, - commonProperties: resolveCommonProperties(product.commit, product.version, stateService.getItem('telemetry.machineId'), product.msftInternalDomains, installSourcePath), + commonProperties: resolveCommonProperties(fileService, release(), process.arch, product.commit, product.version, stateService.getItem('telemetry.machineId'), product.msftInternalDomains, installSourcePath), piiPaths: [appRoot, extensionsPath] }; @@ -434,17 +166,70 @@ export async function main(argv: NativeParsedArgs): Promise { services.set(ITelemetryService, NullTelemetryService); } - const instantiationService2 = instantiationService.createChild(services); - const main = instantiationService2.createInstance(Main); + return [new InstantiationService(services), appenders]; + } - try { - await main.run(argv); + private registerErrorHandler(logService: ILogService): void { - // Flush the remaining data in AI adapter. - // If it does not complete in 1 second, exit the process. - await raceTimeout(combinedAppender(...appenders).flush(), 1000); - } finally { - disposables.dispose(); + // Install handler for unexpected errors + setUnexpectedErrorHandler(error => { + const message = toErrorMessage(error, true); + if (!message) { + return; + } + + logService.error(`[uncaught exception in CLI]: ${message}`); + }); + } + + private async doRun(environmentService: INativeEnvironmentService, extensionManagementCLIService: IExtensionManagementCLIService): Promise { + + // Install Source + if (this.argv['install-source']) { + return this.setInstallSource(environmentService, this.argv['install-source']); } - }); + + // List Extensions + if (this.argv['list-extensions']) { + return extensionManagementCLIService.listExtensions(!!this.argv['show-versions'], this.argv['category']); + } + + // Install Extension + else if (this.argv['install-extension'] || this.argv['install-builtin-extension']) { + return extensionManagementCLIService.installExtensions(this.asExtensionIdOrVSIX(this.argv['install-extension'] || []), this.argv['install-builtin-extension'] || [], !!this.argv['do-not-sync'], !!this.argv['force']); + } + + // Uninstall Extension + else if (this.argv['uninstall-extension']) { + return extensionManagementCLIService.uninstallExtensions(this.asExtensionIdOrVSIX(this.argv['uninstall-extension']), !!this.argv['force']); + } + + // Locate Extension + else if (this.argv['locate-extension']) { + return extensionManagementCLIService.locateExtension(this.argv['locate-extension']); + } + + // Telemetry + else if (this.argv['telemetry']) { + console.log(buildTelemetryMessage(environmentService.appRoot, environmentService.extensionsPath)); + } + } + + private asExtensionIdOrVSIX(inputs: string[]): (string | URI)[] { + return inputs.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(process.cwd(), input)) : input); + } + + private setInstallSource(environmentService: INativeEnvironmentService, installSource: string): Promise { + return writeFile(environmentService.installSourcePath, installSource.slice(0, 30)); + } +} + +export async function main(argv: NativeParsedArgs): Promise { + const cliMain = new CliMain(argv); + + try { + await cliMain.run(); + } finally { + cliMain.dispose(); + } } diff --git a/src/vs/code/node/shellEnv.ts b/src/vs/code/node/shellEnv.ts index 29ef8c14..9454f1d4 100644 --- a/src/vs/code/node/shellEnv.ts +++ b/src/vs/code/node/shellEnv.ts @@ -5,11 +5,12 @@ import { spawn } from 'child_process'; import { generateUuid } from 'vs/base/common/uuid'; -import { isWindows } from 'vs/base/common/platform'; +import { isWindows, platform } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper'; import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { getSystemShell } from 'vs/base/node/shell'; /** * We need to get the environment from a user's shell. @@ -58,7 +59,7 @@ export async function resolveShellEnv(logService: ILogService, args: NativeParse let unixShellEnvPromise: Promise | undefined = undefined; async function doResolveUnixShellEnv(logService: ILogService): Promise { - const promise = new Promise((resolve, reject) => { + const promise = new Promise(async (resolve, reject) => { const runAsNode = process.env['ELECTRON_RUN_AS_NODE']; logService.trace('getUnixShellEnvironment#runAsNode', runAsNode); @@ -78,7 +79,8 @@ async function doResolveUnixShellEnv(logService: ILogService): Promise 2000); return new FontInfo({ zoomLevel: browser.getZoomLevel(), + pixelRatio: browser.getPixelRatio(), fontFamily: bareFontInfo.fontFamily, fontWeight: bareFontInfo.fontWeight, fontSize: bareFontInfo.fontSize, @@ -331,15 +333,15 @@ export class Configuration extends CommonEditorConfiguration { constructor( isSimpleWidget: boolean, - options: IEditorConstructionOptions, + options: Readonly, referenceDomElement: HTMLElement | null = null, private readonly accessibilityService: IAccessibilityService ) { super(isSimpleWidget, options); - this._elementSizeObserver = this._register(new ElementSizeObserver(referenceDomElement, options.dimension, () => this._onReferenceDomElementSizeChanged())); + this._elementSizeObserver = this._register(new ElementSizeObserver(referenceDomElement, options.dimension, () => this._recomputeOptions())); - this._register(CSSBasedConfiguration.INSTANCE.onDidChange(() => this._onCSSBasedConfigurationChanged())); + this._register(CSSBasedConfiguration.INSTANCE.onDidChange(() => this._recomputeOptions())); if (this._validatedOptions.get(EditorOption.automaticLayout)) { this._elementSizeObserver.startObserving(); @@ -351,28 +353,24 @@ export class Configuration extends CommonEditorConfiguration { this._recomputeOptions(); } - private _onReferenceDomElementSizeChanged(): void { - this._recomputeOptions(); - } - - private _onCSSBasedConfigurationChanged(): void { - this._recomputeOptions(); - } - public observeReferenceElement(dimension?: IDimension): void { this._elementSizeObserver.observe(dimension); } - public dispose(): void { - super.dispose(); + public updatePixelRatio(): void { + this._recomputeOptions(); } - private _getExtraEditorClassName(): string { + private static _getExtraEditorClassName(): string { let extra = ''; if (!browser.isSafari && !browser.isWebkitWebView) { // Use user-select: none in all browsers except Safari and native macOS WebView extra += 'no-user-select '; } + if (browser.isSafari) { + // See https://github.com/microsoft/vscode/issues/108822 + extra += 'no-minimap-shadow '; + } if (platform.isMacintosh) { extra += 'mac '; } @@ -381,7 +379,7 @@ export class Configuration extends CommonEditorConfiguration { protected _getEnvConfiguration(): IEnvConfiguration { return { - extraEditorClassName: this._getExtraEditorClassName(), + extraEditorClassName: Configuration._getExtraEditorClassName(), outerWidth: this._elementSizeObserver.getWidth(), outerHeight: this._elementSizeObserver.getHeight(), emptySelectionClipboard: browser.isWebKit || browser.isFirefox, diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index 06eecaa6..31f3a0b6 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -110,7 +110,12 @@ export class MouseHandler extends ViewEventHandler { return; } const e = new StandardWheelEvent(browserEvent); - if (e.browserEvent!.ctrlKey || e.browserEvent!.metaKey) { + const doMouseWheelZoom = ( + platform.isMacintosh + ? (browserEvent.metaKey && !browserEvent.ctrlKey && !browserEvent.shiftKey && !browserEvent.altKey) + : (browserEvent.ctrlKey && !browserEvent.metaKey && !browserEvent.shiftKey && !browserEvent.altKey) + ); + if (doMouseWheelZoom) { const zoomLevel: number = EditorZoom.getZoomLevel(); const delta = e.deltaY > 0 ? 1 : -1; EditorZoom.setZoomLevel(zoomLevel + delta); diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index 37c1d8b0..ada5f46a 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -269,11 +269,11 @@ export class HitTestContext { const viewZoneWhitespace = context.viewLayout.getWhitespaceAtVerticalOffset(mouseVerticalOffset); if (viewZoneWhitespace) { - let viewZoneMiddle = viewZoneWhitespace.verticalOffset + viewZoneWhitespace.height / 2, - lineCount = context.model.getLineCount(), - positionBefore: Position | null = null, - position: Position | null, - positionAfter: Position | null = null; + const viewZoneMiddle = viewZoneWhitespace.verticalOffset + viewZoneWhitespace.height / 2; + const lineCount = context.model.getLineCount(); + let positionBefore: Position | null = null; + let position: Position | null; + let positionAfter: Position | null = null; if (viewZoneWhitespace.afterLineNumber !== lineCount) { // There are more lines after this view zone @@ -767,7 +767,7 @@ export class MouseTargetFactory { const lineWidth = ctx.getLineWidth(lineNumber); if (request.mouseContentHorizontalOffset > lineWidth) { - if (browser.isEdge && pos.column === 1) { + if (browser.isEdgeLegacy && pos.column === 1) { // See https://github.com/microsoft/vscode/issues/10875 const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth); return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(lineNumber, ctx.model.getLineMaxColumn(lineNumber)), undefined, detail); @@ -940,12 +940,16 @@ export class MouseTargetFactory { } } - // For inline decorations, Gecko returns the `` of the line and the offset is the `` with the inline decoration + // For inline decorations, Gecko sometimes returns the `` of the line and the offset is the `` with the inline decoration + // Some other times, it returns the `` with the inline decoration if (hitResult.offsetNode.nodeType === hitResult.offsetNode.ELEMENT_NODE) { - const parent1 = hitResult.offsetNode.parentNode; // expected to be the view line div + const parent1 = hitResult.offsetNode.parentNode; const parent1ClassName = parent1 && parent1.nodeType === parent1.ELEMENT_NODE ? (parent1).className : null; + const parent2 = parent1 ? parent1.parentNode : null; + const parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (parent2).className : null; if (parent1ClassName === ViewLine.CLASS_NAME) { + // it returned the `` of the line and the offset is the `` with the inline decoration const tokenSpan = hitResult.offsetNode.childNodes[Math.min(hitResult.offset, hitResult.offsetNode.childNodes.length - 1)]; if (tokenSpan) { const p = ctx.getPositionFromDOMInfo(tokenSpan, 0); @@ -954,6 +958,13 @@ export class MouseTargetFactory { hitTarget: null }; } + } else if (parent2ClassName === ViewLine.CLASS_NAME) { + // it returned the `` with the inline decoration + const p = ctx.getPositionFromDOMInfo(hitResult.offsetNode, 0); + return { + position: p, + hitTarget: null + }; } } @@ -1014,12 +1025,11 @@ export class MouseTargetFactory { } private static _snapToSoftTabBoundary(position: Position, viewModel: IViewModel): Position { - const minColumn = viewModel.getLineMinColumn(position.lineNumber); const lineContent = viewModel.getLineContent(position.lineNumber); const { tabSize } = viewModel.getTextModelOptions(); - const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, position.column - minColumn, tabSize, Direction.Nearest); + const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, position.column - 1, tabSize, Direction.Nearest); if (newPosition !== -1) { - return new Position(position.lineNumber, newPosition + minColumn); + return new Position(position.lineNumber, newPosition + 1); } return position; } diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index 2f26618a..b2f38304 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -54,7 +54,7 @@ class VisibleTextAreaData { } } -const canUseZeroSizeTextarea = (browser.isEdge || browser.isFirefox); +const canUseZeroSizeTextarea = (browser.isFirefox); export class TextAreaHandler extends ViewPart { @@ -280,14 +280,8 @@ export class TextAreaHandler extends ViewPart { })); this._register(this._textAreaInput.onCompositionUpdate((e: ICompositionData) => { - if (browser.isEdge) { - // Due to isEdgeOrIE (where the textarea was not cleared initially) - // we cannot assume the text consists only of the composited text - this._visibleTextArea = this._visibleTextArea!.setWidth(0); - } else { - // adjust width by its size - this._visibleTextArea = this._visibleTextArea!.setWidth(measureText(e.data, this._fontInfo)); - } + // adjust width by its size + this._visibleTextArea = this._visibleTextArea!.setWidth(measureText(e.data, this._fontInfo)); this._render(); })); diff --git a/src/vs/editor/browser/controller/textAreaInput.ts b/src/vs/editor/browser/controller/textAreaInput.ts index bc1e26c1..60627bab 100644 --- a/src/vs/editor/browser/controller/textAreaInput.ts +++ b/src/vs/editor/browser/controller/textAreaInput.ts @@ -13,7 +13,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import * as strings from 'vs/base/common/strings'; -import { ITextAreaWrapper, ITypeData, TextAreaState } from 'vs/editor/browser/controller/textAreaState'; +import { ITextAreaWrapper, ITypeData, TextAreaState, _debugComposition } from 'vs/editor/browser/controller/textAreaState'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; @@ -147,6 +147,7 @@ export class TextAreaInput extends Disposable { private readonly _host: ITextAreaInputHost; private readonly _textArea: TextAreaWrapper; private readonly _asyncTriggerCut: RunOnceScheduler; + private readonly _asyncFocusGainWriteScreenReaderContent: RunOnceScheduler; private _textAreaState: TextAreaState; private _selectionChangeListener: IDisposable | null; @@ -160,6 +161,7 @@ export class TextAreaInput extends Disposable { this._host = host; this._textArea = this._register(new TextAreaWrapper(textArea)); this._asyncTriggerCut = this._register(new RunOnceScheduler(() => this._onCut.fire(), 0)); + this._asyncFocusGainWriteScreenReaderContent = this._register(new RunOnceScheduler(() => this.writeScreenReaderContent('asyncFocusGain'), 0)); this._textAreaState = TextAreaState.EMPTY; this._selectionChangeListener = null; @@ -193,6 +195,10 @@ export class TextAreaInput extends Disposable { })); this._register(dom.addDisposableListener(textArea.domNode, 'compositionstart', (e: CompositionEvent) => { + if (_debugComposition) { + console.log(`[compositionstart]`, e); + } + if (this._isDoingComposition) { return; } @@ -209,6 +215,9 @@ export class TextAreaInput extends Disposable { ) { // Handling long press case on macOS + arrow key => pretend the character was selected if (lastKeyDown.code === 'ArrowRight' || lastKeyDown.code === 'ArrowLeft') { + if (_debugComposition) { + console.log(`[compositionstart] Handling long press case on macOS + arrow key`, e); + } moveOneCharacterLeft = true; } } @@ -221,8 +230,7 @@ export class TextAreaInput extends Disposable { this._textAreaState.selectionStartPosition ? new Position(this._textAreaState.selectionStartPosition.lineNumber, this._textAreaState.selectionStartPosition.column - 1) : null, this._textAreaState.selectionEndPosition ); - } else if (!browser.isEdge) { - // In IE we cannot set .value when handling 'compositionstart' because the entire composition will get canceled. + } else { this._setAndWriteTextAreaState('compositionstart', TextAreaState.EMPTY); } @@ -251,27 +259,10 @@ export class TextAreaInput extends Disposable { return [newState, typeInput]; }; - const compositionDataInValid = (locale: string): boolean => { - // https://github.com/microsoft/monaco-editor/issues/339 - // Multi-part Japanese compositions reset cursor in Edge/IE, Chinese and Korean IME don't have this issue. - // The reason that we can't use this path for all CJK IME is IE and Edge behave differently when handling Korean IME, - // which breaks this path of code. - if (browser.isEdge && locale === 'ja') { - return true; - } - - return false; - }; - this._register(dom.addDisposableListener(textArea.domNode, 'compositionupdate', (e: CompositionEvent) => { - if (compositionDataInValid(e.locale)) { - const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false); - this._textAreaState = newState; - this._onType.fire(typeInput); - this._onCompositionUpdate.fire(e); - return; + if (_debugComposition) { + console.log(`[compositionupdate]`, e); } - const [newState, typeInput] = deduceComposition(e.data || ''); this._textAreaState = newState; this._onType.fire(typeInput); @@ -279,28 +270,23 @@ export class TextAreaInput extends Disposable { })); this._register(dom.addDisposableListener(textArea.domNode, 'compositionend', (e: CompositionEvent) => { + if (_debugComposition) { + console.log(`[compositionend]`, e); + } // https://github.com/microsoft/monaco-editor/issues/1663 // On iOS 13.2, Chinese system IME randomly trigger an additional compositionend event with empty data if (!this._isDoingComposition) { return; } - if (compositionDataInValid(e.locale)) { - // https://github.com/microsoft/monaco-editor/issues/339 - const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false); - this._textAreaState = newState; - this._onType.fire(typeInput); - } else { - const [newState, typeInput] = deduceComposition(e.data || ''); - this._textAreaState = newState; - this._onType.fire(typeInput); - } - // Due to - // isEdgeOrIE (where the textarea was not cleared initially) - // and isChrome (the textarea is not updated correctly when composition ends) - // and isFirefox (the textare ais not updated correctly after inserting emojis) - // we cannot assume the text at the end consists only of the composited text - if (browser.isEdge || browser.isChrome || browser.isFirefox) { + const [newState, typeInput] = deduceComposition(e.data || ''); + this._textAreaState = newState; + this._onType.fire(typeInput); + + // isChrome: the textarea is not updated correctly when composition ends + // isFirefox: the textarea is not updated correctly after inserting emojis + // => we cannot assume the text at the end consists only of the composited text + if (browser.isChrome || browser.isFirefox) { this._textAreaState = TextAreaState.readFromTextArea(this._textArea); } @@ -375,9 +361,31 @@ export class TextAreaInput extends Disposable { })); this._register(dom.addDisposableListener(textArea.domNode, 'focus', () => { + const hadFocus = this._hasFocus; + this._setHasFocus(true); + + if (browser.isSafari && !hadFocus && this._hasFocus) { + // When "tabbing into" the textarea, immediately after dispatching the 'focus' event, + // Safari will always move the selection at offset 0 in the textarea + this._asyncFocusGainWriteScreenReaderContent.schedule(); + } })); this._register(dom.addDisposableListener(textArea.domNode, 'blur', () => { + if (this._isDoingComposition) { + // See https://github.com/microsoft/vscode/issues/112621 + // where compositionend is not triggered when the editor + // is taken off-dom during a composition + + // Clear the flag to be able to write to the textarea + this._isDoingComposition = false; + + // Clear the textarea to avoid an unwanted cursor type + this.writeScreenReaderContent('blurWithoutCompositionEnd'); + + // Fire artificial composition end + this._onCompositionEnd.fire(); + } this._setHasFocus(false); })); } @@ -513,13 +521,7 @@ export class TextAreaInput extends Disposable { } if (this._hasFocus) { - if (browser.isEdge) { - // Edge has a bug where setting the selection range while the focus event - // is dispatching doesn't work. To reproduce, "tab into" the editor. - this._setAndWriteTextAreaState('focusgain', TextAreaState.EMPTY); - } else { - this.writeScreenReaderContent('focusgain'); - } + this.writeScreenReaderContent('focusgain'); } if (this._hasFocus) { diff --git a/src/vs/editor/browser/controller/textAreaState.ts b/src/vs/editor/browser/controller/textAreaState.ts index 65c815e3..d8a2f201 100644 --- a/src/vs/editor/browser/controller/textAreaState.ts +++ b/src/vs/editor/browser/controller/textAreaState.ts @@ -8,6 +8,8 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { EndOfLinePreference } from 'vs/editor/common/model'; +export const _debugComposition = false; + export interface ITextAreaWrapper { getValue(): string; setValue(reason: string, value: string): void; @@ -59,7 +61,9 @@ export class TextAreaState { } public writeToTextArea(reason: string, textArea: ITextAreaWrapper, select: boolean): void { - // console.log(Date.now() + ': writeToTextArea ' + reason + ': ' + this.toString()); + if (_debugComposition) { + console.log('writeToTextArea ' + reason + ': ' + this.toString()); + } textArea.setValue(reason, this.value); if (select) { textArea.setSelectionRange(reason, this.selectionStart, this.selectionEnd); @@ -105,9 +109,11 @@ export class TextAreaState { }; } - // console.log('------------------------deduceInput'); - // console.log('PREVIOUS STATE: ' + previousState.toString()); - // console.log('CURRENT STATE: ' + currentState.toString()); + if (_debugComposition) { + console.log('------------------------deduceInput'); + console.log('PREVIOUS STATE: ' + previousState.toString()); + console.log('CURRENT STATE: ' + currentState.toString()); + } let previousValue = previousState.value; let previousSelectionStart = previousState.selectionStart; @@ -133,8 +139,10 @@ export class TextAreaState { currentSelectionEnd -= prefixLength; previousSelectionEnd -= prefixLength; - // console.log('AFTER DIFFING PREVIOUS STATE: <' + previousValue + '>, selectionStart: ' + previousSelectionStart + ', selectionEnd: ' + previousSelectionEnd); - // console.log('AFTER DIFFING CURRENT STATE: <' + currentValue + '>, selectionStart: ' + currentSelectionStart + ', selectionEnd: ' + currentSelectionEnd); + if (_debugComposition) { + console.log('AFTER DIFFING PREVIOUS STATE: <' + previousValue + '>, selectionStart: ' + previousSelectionStart + ', selectionEnd: ' + previousSelectionEnd); + console.log('AFTER DIFFING CURRENT STATE: <' + currentValue + '>, selectionStart: ' + currentSelectionStart + ', selectionEnd: ' + currentSelectionEnd); + } if (couldBeEmojiInput && currentSelectionStart === currentSelectionEnd && previousValue.length > 0) { // on OSX, emojis from the emoji picker are inserted at random locations @@ -196,7 +204,9 @@ export class TextAreaState { // no current selection const replacePreviousCharacters = (previousPrefix.length - prefixLength); - // console.log('REMOVE PREVIOUS: ' + (previousPrefix.length - prefixLength) + ' chars'); + if (_debugComposition) { + console.log('REMOVE PREVIOUS: ' + (previousPrefix.length - prefixLength) + ' chars'); + } return { text: currentValue, diff --git a/src/vs/editor/browser/core/markdownRenderer.ts b/src/vs/editor/browser/core/markdownRenderer.ts index e1eb76c8..ec24b6b5 100644 --- a/src/vs/editor/browser/core/markdownRenderer.ts +++ b/src/vs/editor/browser/core/markdownRenderer.ts @@ -88,9 +88,7 @@ export class MarkdownRenderer { const element = document.createElement('span'); - element.innerHTML = MarkdownRenderer._ttpTokenizer - ? MarkdownRenderer._ttpTokenizer.createHTML(value, tokenization) as unknown as string - : tokenizeToString(value, tokenization); + element.innerHTML = (MarkdownRenderer._ttpTokenizer?.createHTML(value, tokenization) ?? tokenizeToString(value, tokenization)) as string; // use "good" font let fontFamily = this._options.codeBlockFontFamily; @@ -105,7 +103,7 @@ export class MarkdownRenderer { }, asyncRenderCallback: () => this._onDidRenderAsync.fire(), actionHandler: { - callback: (content) => this._openerService.open(content, { fromUserGesture: true }).catch(onUnexpectedError), + callback: (content) => this._openerService.open(content, { fromUserGesture: true, allowContributedOpeners: true }).catch(onUnexpectedError), disposeables } }; diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 43305c28..bfa07b8d 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -361,6 +361,11 @@ export interface IEditorConstructionOptions extends IEditorOptions { } export interface IDiffEditorConstructionOptions extends IDiffEditorOptions { + /** + * The initial editor dimension (to avoid measuring the container). + */ + dimension?: editorCommon.IDimension; + /** * Place overflow widgets inside an external DOM node. * Defaults to an internal DOM node. @@ -1047,7 +1052,7 @@ export interface IDiffEditor extends editorCommon.IEditor { /** *@internal */ -export function isCodeEditor(thing: any): thing is ICodeEditor { +export function isCodeEditor(thing: unknown): thing is ICodeEditor { if (thing && typeof (thing).getEditorType === 'function') { return (thing).getEditorType() === editorCommon.EditorType.ICodeEditor; } else { @@ -1058,7 +1063,7 @@ export function isCodeEditor(thing: any): thing is ICodeEditor { /** *@internal */ -export function isDiffEditor(thing: any): thing is IDiffEditor { +export function isDiffEditor(thing: unknown): thing is IDiffEditor { if (thing && typeof (thing).getEditorType === 'function') { return (thing).getEditorType() === editorCommon.EditorType.IDiffEditor; } else { @@ -1069,8 +1074,8 @@ export function isDiffEditor(thing: any): thing is IDiffEditor { /** *@internal */ -export function isCompositeEditor(thing: any): thing is editorCommon.ICompositeCodeEditor { - return thing +export function isCompositeEditor(thing: unknown): thing is editorCommon.ICompositeCodeEditor { + return !!thing && typeof thing === 'object' && typeof (thing).onDidChangeActiveEditor === 'function'; @@ -1079,7 +1084,7 @@ export function isCompositeEditor(thing: any): thing is editorCommon.ICompositeC /** *@internal */ -export function getCodeEditor(thing: any): ICodeEditor | null { +export function getCodeEditor(thing: unknown): ICodeEditor | null { if (isCodeEditor(thing)) { return thing; } diff --git a/src/vs/editor/browser/services/bulkEditService.ts b/src/vs/editor/browser/services/bulkEditService.ts index a9e8b99d..5a49c330 100644 --- a/src/vs/editor/browser/services/bulkEditService.ts +++ b/src/vs/editor/browser/services/bulkEditService.ts @@ -69,11 +69,11 @@ export interface IBulkEditOptions { progress?: IProgress; token?: CancellationToken; showPreview?: boolean; - suppressPreview?: boolean; label?: string; quotableLabel?: string; undoRedoSource?: UndoRedoSource; undoRedoGroupId?: number; + confirmBeforeUndo?: boolean; } export interface IBulkEditResult { diff --git a/src/vs/editor/browser/services/codeEditorServiceImpl.ts b/src/vs/editor/browser/services/codeEditorServiceImpl.ts index c961bfd7..952b317c 100644 --- a/src/vs/editor/browser/services/codeEditorServiceImpl.ts +++ b/src/vs/editor/browser/services/codeEditorServiceImpl.ts @@ -347,6 +347,8 @@ const _CSS_MAP: { [prop: string]: string; } = { fontStyle: 'font-style:{0};', fontWeight: 'font-weight:{0};', + fontSize: 'font-size:{0};', + fontFamily: 'font-family:{0};', textDecoration: 'text-decoration:{0};', cursor: 'cursor:{0};', letterSpacing: 'letter-spacing:{0};', @@ -357,6 +359,7 @@ const _CSS_MAP: { [prop: string]: string; } = { contentText: 'content:\'{0}\';', contentIconPath: 'content:{0};', margin: 'margin:{0};', + padding: 'padding:{0};', width: 'width:{0};', height: 'height:{0};' }; @@ -529,7 +532,7 @@ class DecorationCSSRules { cssTextArr.push(strings.format(_CSS_MAP.contentText, escaped)); } - this.collectCSSText(opts, ['fontStyle', 'fontWeight', 'textDecoration', 'color', 'opacity', 'backgroundColor', 'margin'], cssTextArr); + this.collectCSSText(opts, ['fontStyle', 'fontWeight', 'fontSize', 'fontFamily', 'textDecoration', 'color', 'opacity', 'backgroundColor', 'margin', 'padding'], cssTextArr); if (this.collectCSSText(opts, ['width', 'height'], cssTextArr)) { cssTextArr.push('display:inline-block;'); } diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index 1360871f..dd329d44 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -4,18 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable } from 'vs/base/common/lifecycle'; import { LinkedList } from 'vs/base/common/linkedList'; +import { ResourceMap } from 'vs/base/common/map'; import { parse } from 'vs/base/common/marshalling'; import { Schemas } from 'vs/base/common/network'; import { normalizePath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IOpener, IOpenerService, IValidator, IExternalUriResolver, OpenOptions, ResolveExternalUriOptions, IResolvedExternalUri, IExternalOpener, matchesScheme } from 'vs/platform/opener/common/opener'; import { EditorOpenContext } from 'vs/platform/editor/common/editor'; -import { ResourceMap } from 'vs/base/common/map'; - +import { IExternalOpener, IExternalUriResolver, IOpener, IOpenerService, IResolvedExternalUri, IValidator, matchesScheme, OpenOptions, ResolveExternalUriOptions } from 'vs/platform/opener/common/opener'; class CommandOpener implements IOpener { @@ -100,15 +100,16 @@ export class OpenerService implements IOpenerService { private readonly _resolvers = new LinkedList(); private readonly _resolvedUriTargets = new ResourceMap(uri => uri.with({ path: null, fragment: null, query: null }).toString()); - private _externalOpener: IExternalOpener; + private _defaultExternalOpener: IExternalOpener; + private readonly _externalOpeners = new LinkedList(); constructor( @ICodeEditorService editorService: ICodeEditorService, @ICommandService commandService: ICommandService, ) { // Default external opener is going through window.open() - this._externalOpener = { - openExternal: href => { + this._defaultExternalOpener = { + openExternal: async href => { // ensure to open HTTP/HTTPS links into new windows // to not trigger a navigation. Any other link is // safe to be set as HREF to prevent a blank window @@ -118,11 +119,11 @@ export class OpenerService implements IOpenerService { } else { window.location.href = href; } - return Promise.resolve(true); + return true; } }; - // Default opener: maito, http(s), command, and catch-all-editors + // Default opener: any external, maito, http(s), command, and catch-all-editors this._openers.push({ open: async (target: URI | string, options?: OpenOptions) => { if (options?.openExternal || matchesScheme(target, Schemas.mailto) || matchesScheme(target, Schemas.http) || matchesScheme(target, Schemas.https)) { @@ -152,8 +153,13 @@ export class OpenerService implements IOpenerService { return { dispose: remove }; } - setExternalOpener(externalOpener: IExternalOpener): void { - this._externalOpener = externalOpener; + setDefaultExternalOpener(externalOpener: IExternalOpener): void { + this._defaultExternalOpener = externalOpener; + } + + registerExternalOpener(opener: IExternalOpener): IDisposable { + const remove = this._externalOpeners.push(opener); + return { dispose: remove }; } async open(target: URI | string, options?: OpenOptions): Promise { @@ -196,13 +202,29 @@ export class OpenerService implements IOpenerService { const uri = typeof resource === 'string' ? URI.parse(resource) : resource; const { resolved } = await this.resolveExternalUri(uri, options); + let href: string; if (typeof resource === 'string' && uri.toString() === resolved.toString()) { // open the url-string AS IS - return this._externalOpener.openExternal(resource); + href = resource; } else { // open URI using the toString(noEncode)+encodeURI-trick - return this._externalOpener.openExternal(encodeURI(resolved.toString(true))); + href = encodeURI(resolved.toString(true)); } + + if (options?.allowContributedOpeners) { + const preferredOpenerId = typeof options?.allowContributedOpeners === 'string' ? options?.allowContributedOpeners : undefined; + for (const opener of this._externalOpeners) { + const didOpen = await opener.openExternal(href, { + sourceUri: uri, + preferredOpenerId, + }, CancellationToken.None); + if (didOpen) { + return true; + } + } + } + + return this._defaultExternalOpener.openExternal(href, { sourceUri: uri }, CancellationToken.None); } dispose() { diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 87c6461d..cd486fc2 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -111,8 +111,8 @@ function createLineBreaks(requests: string[], fontInfo: FontInfo, tabSize: numbe allVisibleColumns[i] = tmp[1]; } const html = sb.build(); - const trustedhtml = ttPolicy ? ttPolicy.createHTML(html) : html; - containerDomNode.innerHTML = trustedhtml as unknown as string; + const trustedhtml = ttPolicy?.createHTML(html) ?? html; + containerDomNode.innerHTML = trustedhtml as string; containerDomNode.style.position = 'absolute'; containerDomNode.style.top = '10000'; diff --git a/src/vs/editor/browser/view/viewImpl.ts b/src/vs/editor/browser/view/viewImpl.ts index b656a3f8..e0620c2a 100644 --- a/src/vs/editor/browser/view/viewImpl.ts +++ b/src/vs/editor/browser/view/viewImpl.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import * as browser from 'vs/base/browser/browser'; import { Selection } from 'vs/editor/common/core/selection'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; @@ -65,6 +66,7 @@ export class View extends ViewEventHandler { private readonly _scrollbar: EditorScrollbar; private readonly _context: ViewContext; + private _configPixelRatio: number; private _selections: Selection[]; // The view lines @@ -104,6 +106,7 @@ export class View extends ViewEventHandler { // The view context is passed on to most classes (basically to reduce param. counts in ctors) this._context = new ViewContext(configuration, themeService.getColorTheme(), model); + this._configPixelRatio = this._configPixelRatio = this._context.configuration.options.get(EditorOption.pixelRatio); // Ensure the view is the first event handler in order to update the layout this._context.addEventHandler(this); @@ -298,6 +301,7 @@ export class View extends ViewEventHandler { this._scheduleRender(); } public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { + this._configPixelRatio = this._context.configuration.options.get(EditorOption.pixelRatio); this.domNode.setClassName(this._getEditorClassName()); this._applyLayout(); return false; @@ -330,8 +334,8 @@ export class View extends ViewEventHandler { this._viewLines.dispose(); // Destroy view parts - for (let i = 0, len = this._viewParts.length; i < len; i++) { - this._viewParts[i].dispose(); + for (const viewPart of this._viewParts) { + viewPart.dispose(); } super.dispose(); @@ -354,8 +358,7 @@ export class View extends ViewEventHandler { private _getViewPartsToRender(): ViewPart[] { let result: ViewPart[] = [], resultLen = 0; - for (let i = 0, len = this._viewParts.length; i < len; i++) { - const viewPart = this._viewParts[i]; + for (const viewPart of this._viewParts) { if (viewPart.shouldRender()) { result[resultLen++] = viewPart; } @@ -401,16 +404,20 @@ export class View extends ViewEventHandler { const renderingContext = new RenderingContext(this._context.viewLayout, viewportData, this._viewLines); // Render the rest of the parts - for (let i = 0, len = viewPartsToRender.length; i < len; i++) { - const viewPart = viewPartsToRender[i]; + for (const viewPart of viewPartsToRender) { viewPart.prepareRender(renderingContext); } - for (let i = 0, len = viewPartsToRender.length; i < len; i++) { - const viewPart = viewPartsToRender[i]; + for (const viewPart of viewPartsToRender) { viewPart.render(renderingContext); viewPart.onDidRender(); } + + // Try to detect browser zooming and paint again if necessary + if (Math.abs(browser.getPixelRatio() - this._configPixelRatio) > 0.001) { + // looks like the pixel ratio has changed + this._context.configuration.updatePixelRatio(); + } } // --- BEGIN CodeEditor helpers @@ -462,8 +469,7 @@ export class View extends ViewEventHandler { if (everything) { // Force everything to render... this._viewLines.forceShouldRender(); - for (let i = 0, len = this._viewParts.length; i < len; i++) { - const viewPart = this._viewParts[i]; + for (const viewPart of this._viewParts) { viewPart.forceShouldRender(); } } diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index 89de8468..0a1a7ddc 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -506,15 +506,15 @@ class ViewLayerRenderer { ctx.lines.splice(removeIndex, removeCount); } - private _finishRenderingNewLines(ctx: IRendererContext, domNodeIsEmpty: boolean, newLinesHTML: string, wasNew: boolean[]): void { + private _finishRenderingNewLines(ctx: IRendererContext, domNodeIsEmpty: boolean, newLinesHTML: string | TrustedHTML, wasNew: boolean[]): void { if (ViewLayerRenderer._ttPolicy) { - newLinesHTML = ViewLayerRenderer._ttPolicy.createHTML(newLinesHTML) as unknown as string; // explains the ugly casts -> https://github.com/microsoft/vscode/issues/106396#issuecomment-692625393 + newLinesHTML = ViewLayerRenderer._ttPolicy.createHTML(newLinesHTML as string); } const lastChild = this.domNode.lastChild; if (domNodeIsEmpty || !lastChild) { - this.domNode.innerHTML = newLinesHTML; + this.domNode.innerHTML = newLinesHTML as string; // explains the ugly casts -> https://github.com/microsoft/vscode/issues/106396#issuecomment-692625393; } else { - lastChild.insertAdjacentHTML('afterend', newLinesHTML); + lastChild.insertAdjacentHTML('afterend', newLinesHTML as string); } let currChild = this.domNode.lastChild; @@ -527,13 +527,13 @@ class ViewLayerRenderer { } } - private _finishRenderingInvalidLines(ctx: IRendererContext, invalidLinesHTML: string, wasInvalid: boolean[]): void { + private _finishRenderingInvalidLines(ctx: IRendererContext, invalidLinesHTML: string | TrustedHTML, wasInvalid: boolean[]): void { const hugeDomNode = document.createElement('div'); if (ViewLayerRenderer._ttPolicy) { - invalidLinesHTML = ViewLayerRenderer._ttPolicy.createHTML(invalidLinesHTML) as unknown as string; + invalidLinesHTML = ViewLayerRenderer._ttPolicy.createHTML(invalidLinesHTML as string); } - hugeDomNode.innerHTML = invalidLinesHTML; + hugeDomNode.innerHTML = invalidLinesHTML as string; for (let i = 0; i < ctx.linesLength; i++) { const line = ctx.lines[i]; diff --git a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts index 6208c89f..b3ab2991 100644 --- a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts +++ b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts @@ -42,8 +42,8 @@ export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { this._contentWidth = layoutInfo.contentWidth; this._selectionIsEmpty = true; this._focused = false; - this._cursorLineNumbers = []; - this._selections = []; + this._cursorLineNumbers = [1]; + this._selections = [new Selection(1, 1, 1, 1)]; this._renderData = null; this._context.addEventHandler(this); diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index d80556fb..96754ec4 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -44,7 +44,7 @@ const canUseFastRenderedViewLine = (function () { let monospaceAssumptionsAreValid = true; -const alwaysRenderInlineSelection = (browser.isEdge); +const alwaysRenderInlineSelection = (browser.isEdgeLegacy); export class DomReadingContext { diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.css b/src/vs/editor/browser/viewParts/minimap/minimap.css index f3692e7e..f4663ece 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.css +++ b/src/vs/editor/browser/viewParts/minimap/minimap.css @@ -25,3 +25,8 @@ left: -6px; width: 6px; } +.monaco-editor.no-minimap-shadow .minimap-shadow-visible { + position: absolute; + left: -1px; + width: 1px; +} diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index 7ca33211..348315fa 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -862,6 +862,7 @@ export class Minimap extends ViewPart implements IMinimapModel { } } public onTokensColorsChanged(e: viewEvents.ViewTokensColorsChangedEvent): boolean { + this._onOptionsMaybeChanged(); return this._actual.onTokensColorsChanged(); } public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean { diff --git a/src/vs/editor/browser/viewParts/selections/selections.ts b/src/vs/editor/browser/viewParts/selections/selections.ts index cdb00570..81417f0b 100644 --- a/src/vs/editor/browser/viewParts/selections/selections.ts +++ b/src/vs/editor/browser/viewParts/selections/selections.ts @@ -60,7 +60,7 @@ function toStyled(item: LineVisibleRanges): LineVisibleRangesWithStyle { // TODO@Alex: Remove this once IE11 fixes Bug #524217 // The problem in IE11 is that it does some sort of auto-zooming to accomodate for displays with different pixel density. // Unfortunately, this auto-zooming is buggy around dealing with rounded borders -const isIEWithZoomingIssuesNearRoundedBorders = browser.isEdge; +const isIEWithZoomingIssuesNearRoundedBorders = browser.isEdgeLegacy; export class SelectionsOverlay extends DynamicViewOverlay { diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 4c87ce2b..15cd2f34 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -24,7 +24,7 @@ import { ViewUserInputEvents } from 'vs/editor/browser/view/viewUserInputEvents' import { ConfigurationChangedEvent, EditorLayoutInfo, IEditorOptions, EditorOption, IComputedEditorOptions, FindComputedEditorOptionValueById, filterValidationDecorations } from 'vs/editor/common/config/editorOptions'; import { Cursor } from 'vs/editor/common/controller/cursor'; import { CursorColumns } from 'vs/editor/common/controller/cursorCommon'; -import { ICursorPositionChangedEvent, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; +import { CursorChangeReason, ICursorPositionChangedEvent, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; @@ -241,7 +241,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE constructor( domElement: HTMLElement, - options: editorBrowser.IEditorConstructionOptions, + _options: Readonly, codeEditorWidgetOptions: ICodeEditorWidgetOptions, @IInstantiationService instantiationService: IInstantiationService, @ICodeEditorService codeEditorService: ICodeEditorService, @@ -253,10 +253,11 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE ) { super(); - options = options || {}; + const options = { ..._options }; this._domElement = domElement; this._overflowWidgetsDomNode = options.overflowWidgetsDomNode; + delete options.overflowWidgetsDomNode; this._id = (++EDITOR_ID); this._decorationTypeKeysToIds = {}; this._decorationTypeSubtypes = {}; @@ -331,7 +332,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._codeEditorService.addCodeEditor(this); } - protected _createConfiguration(options: editorBrowser.IEditorConstructionOptions, accessibilityService: IAccessibilityService): editorCommon.IConfiguration { + protected _createConfiguration(options: Readonly, accessibilityService: IAccessibilityService): editorCommon.IConfiguration { return new Configuration(this.isSimpleWidget, options, this._domElement, accessibilityService); } @@ -366,7 +367,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return this._instantiationService.invokeFunction(fn); } - public updateOptions(newOptions: IEditorOptions): void { + public updateOptions(newOptions: Readonly): void { this._configuration.updateOptions(newOptions); } @@ -811,7 +812,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE ); } - public setSelections(ranges: readonly ISelection[], source: string = 'api'): void { + public setSelections(ranges: readonly ISelection[], source: string = 'api', reason = CursorChangeReason.NotSet): void { if (!this._modelData) { return; } @@ -823,7 +824,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE throw new Error('Invalid arguments'); } } - this._modelData.viewModel.setSelections(source, ranges); + this._modelData.viewModel.setSelections(source, ranges, reason); } public getContentWidth(): number { @@ -1834,6 +1835,7 @@ export class EditorModeContext extends Disposable { private readonly _hasMultipleDocumentFormattingProvider: IContextKey; private readonly _hasMultipleDocumentSelectionFormattingProvider: IContextKey; private readonly _hasSignatureHelpProvider: IContextKey; + private readonly _hasInlineHintsProvider: IContextKey; private readonly _isInWalkThrough: IContextKey; constructor( @@ -1856,6 +1858,7 @@ export class EditorModeContext extends Disposable { this._hasReferenceProvider = EditorContextKeys.hasReferenceProvider.bindTo(_contextKeyService); this._hasRenameProvider = EditorContextKeys.hasRenameProvider.bindTo(_contextKeyService); this._hasSignatureHelpProvider = EditorContextKeys.hasSignatureHelpProvider.bindTo(_contextKeyService); + this._hasInlineHintsProvider = EditorContextKeys.hasInlineHintsProvider.bindTo(_contextKeyService); this._hasDocumentFormattingProvider = EditorContextKeys.hasDocumentFormattingProvider.bindTo(_contextKeyService); this._hasDocumentSelectionFormattingProvider = EditorContextKeys.hasDocumentSelectionFormattingProvider.bindTo(_contextKeyService); this._hasMultipleDocumentFormattingProvider = EditorContextKeys.hasMultipleDocumentFormattingProvider.bindTo(_contextKeyService); @@ -1884,6 +1887,7 @@ export class EditorModeContext extends Disposable { this._register(modes.DocumentFormattingEditProviderRegistry.onDidChange(update)); this._register(modes.DocumentRangeFormattingEditProviderRegistry.onDidChange(update)); this._register(modes.SignatureHelpProviderRegistry.onDidChange(update)); + this._register(modes.InlineHintsProviderRegistry.onDidChange(update)); update(); } @@ -1935,6 +1939,7 @@ export class EditorModeContext extends Disposable { this._hasReferenceProvider.set(modes.ReferenceProviderRegistry.has(model)); this._hasRenameProvider.set(modes.RenameProviderRegistry.has(model)); this._hasSignatureHelpProvider.set(modes.SignatureHelpProviderRegistry.has(model)); + this._hasInlineHintsProvider.set(modes.InlineHintsProviderRegistry.has(model)); this._hasDocumentFormattingProvider.set(modes.DocumentFormattingEditProviderRegistry.has(model) || modes.DocumentRangeFormattingEditProviderRegistry.has(model)); this._hasDocumentSelectionFormattingProvider.set(modes.DocumentRangeFormattingEditProviderRegistry.has(model)); this._hasMultipleDocumentFormattingProvider.set(modes.DocumentFormattingEditProviderRegistry.all(model).length + modes.DocumentRangeFormattingEditProviderRegistry.all(model).length > 1); diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 0730e4f0..53615b31 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -12,15 +12,14 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import * as objects from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; import { Configuration } from 'vs/editor/browser/config/configuration'; import { StableEditorScrollState } from 'vs/editor/browser/core/editorState'; import * as editorBrowser from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; import { DiffReview } from 'vs/editor/browser/widget/diffReview'; -import { IDiffEditorOptions, IEditorOptions, EditorLayoutInfo, EditorOption, EditorOptions, EditorFontLigatures, stringSet as validateStringSetOption, boolean as validateBooleanOption } from 'vs/editor/common/config/editorOptions'; +import { IDiffEditorOptions, EditorLayoutInfo, EditorOption, EditorOptions, EditorFontLigatures, stringSet as validateStringSetOption, boolean as validateBooleanOption } 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 { ISelection, Selection } from 'vs/editor/common/core/selection'; @@ -54,6 +53,11 @@ import { IViewLineTokens } from 'vs/editor/common/core/lineTokens'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +export interface IDiffCodeEditorWidgetOptions { + originalEditor?: ICodeEditorWidgetOptions; + modifiedEditor?: ICodeEditorWidgetOptions; +} + interface IEditorDiffDecorations { decorations: IModelDeltaDecoration[]; overviewZones: OverviewRulerZone[]; @@ -109,7 +113,7 @@ class VisualEditorState { this._decorations = editor.deltaDecorations(this._decorations, []); } - public apply(editor: CodeEditorWidget, overviewRuler: editorBrowser.IOverviewRuler, newDecorations: IEditorDiffDecorationsWithZones, restoreScrollState: boolean): void { + public apply(editor: CodeEditorWidget, overviewRuler: editorBrowser.IOverviewRuler | null, newDecorations: IEditorDiffDecorationsWithZones, restoreScrollState: boolean): void { const scrollState = restoreScrollState ? StableEditorScrollState.capture(editor) : null; @@ -212,6 +216,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE private _maxComputationTime: number; private _renderIndicators: boolean; private _enableSplitViewResizing: boolean; + private _renderOverviewRuler: boolean; private _strategy!: DiffEditorWidgetStyle; private readonly _updateDecorationsRunner: RunOnceScheduler; @@ -226,7 +231,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE constructor( domElement: HTMLElement, - options: editorBrowser.IDiffEditorConstructionOptions, + options: Readonly, + codeEditorWidgetOptions: IDiffCodeEditorWidgetOptions, @IClipboardService clipboardService: IClipboardService, @IEditorWorkerService editorWorkerService: IEditorWorkerService, @IContextKeyService contextKeyService: IContextKeyService, @@ -287,6 +293,11 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._contextKeyService.createKey('isInEmbeddedDiffEditor', false); } + this._renderOverviewRuler = true; + if (typeof options.renderOverviewRuler !== 'undefined') { + this._renderOverviewRuler = Boolean(options.renderOverviewRuler); + } + this._updateDecorationsRunner = this._register(new RunOnceScheduler(() => this._updateDecorations(), 0)); this._containerDomElement = document.createElement('div'); @@ -308,7 +319,9 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._register(dom.addStandardDisposableListener(this._overviewDomElement, 'mousedown', (e) => { this._modifiedEditor.delegateVerticalScrollbarMouseDown(e); })); - this._containerDomElement.appendChild(this._overviewDomElement); + if (this._renderOverviewRuler) { + this._containerDomElement.appendChild(this._overviewDomElement); + } // Create left side this._originalDomNode = document.createElement('div'); @@ -334,7 +347,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._isVisible = true; this._isHandlingScrollEvent = false; - this._elementSizeObserver = this._register(new ElementSizeObserver(this._containerDomElement, undefined, () => this._onDidContainerSizeChanged())); + this._elementSizeObserver = this._register(new ElementSizeObserver(this._containerDomElement, options.dimension, () => this._onDidContainerSizeChanged())); if (options.automaticLayout) { this._elementSizeObserver.startObserving(); } @@ -353,8 +366,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE rightServices.set(IContextKeyService, rightContextKeyService); const rightScopedInstantiationService = instantiationService.createChild(rightServices); - this._originalEditor = this._createLeftHandSideEditor(options, leftScopedInstantiationService, leftContextKeyService); - this._modifiedEditor = this._createRightHandSideEditor(options, rightScopedInstantiationService, rightContextKeyService); + this._originalEditor = this._createLeftHandSideEditor(options, codeEditorWidgetOptions.originalEditor || {}, leftScopedInstantiationService, leftContextKeyService); + this._modifiedEditor = this._createRightHandSideEditor(options, codeEditorWidgetOptions.modifiedEditor || {}, rightScopedInstantiationService, rightContextKeyService); this._originalOverviewRuler = null; this._modifiedOverviewRuler = null; @@ -415,6 +428,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return this._modifiedEditor.getContentHeight(); } + public getViewWidth(): number { + return this._elementSizeObserver.getWidth(); + } + private _setState(newState: editorBrowser.DiffEditorState): void { if (this._state === newState) { return; @@ -453,6 +470,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } private _recreateOverviewRulers(): void { + if (!this._renderOverviewRuler) { + return; + } + if (this._originalOverviewRuler) { this._overviewDomElement.removeChild(this._originalOverviewRuler.getDomNode()); this._originalOverviewRuler.dispose(); @@ -474,8 +495,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._layoutOverviewRulers(); } - private _createLeftHandSideEditor(options: editorBrowser.IDiffEditorConstructionOptions, instantiationService: IInstantiationService, contextKeyService: IContextKeyService): CodeEditorWidget { - const editor = this._createInnerEditor(instantiationService, this._originalDomNode, this._adjustOptionsForLeftHandSide(options)); + private _createLeftHandSideEditor(options: Readonly, codeEditorWidgetOptions: ICodeEditorWidgetOptions, instantiationService: IInstantiationService, contextKeyService: IContextKeyService): CodeEditorWidget { + const editor = this._createInnerEditor(instantiationService, this._originalDomNode, this._adjustOptionsForLeftHandSide(options), codeEditorWidgetOptions); this._register(editor.onDidScrollChange((e) => { if (this._isHandlingScrollEvent) { @@ -536,8 +557,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return editor; } - private _createRightHandSideEditor(options: editorBrowser.IDiffEditorConstructionOptions, instantiationService: IInstantiationService, contextKeyService: IContextKeyService): CodeEditorWidget { - const editor = this._createInnerEditor(instantiationService, this._modifiedDomNode, this._adjustOptionsForRightHandSide(options)); + private _createRightHandSideEditor(options: Readonly, codeEditorWidgetOptions: ICodeEditorWidgetOptions, instantiationService: IInstantiationService, contextKeyService: IContextKeyService): CodeEditorWidget { + const editor = this._createInnerEditor(instantiationService, this._modifiedDomNode, this._adjustOptionsForRightHandSide(options), codeEditorWidgetOptions); this._register(editor.onDidScrollChange((e) => { if (this._isHandlingScrollEvent) { @@ -604,8 +625,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return editor; } - protected _createInnerEditor(instantiationService: IInstantiationService, container: HTMLElement, options: IEditorOptions): CodeEditorWidget { - return instantiationService.createInstance(CodeEditorWidget, container, options, {}); + protected _createInnerEditor(instantiationService: IInstantiationService, container: HTMLElement, options: Readonly, editorWidgetOptions: ICodeEditorWidgetOptions): CodeEditorWidget { + return instantiationService.createInstance(CodeEditorWidget, container, options, editorWidgetOptions); } public dispose(): void { @@ -627,7 +648,9 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._modifiedOverviewRuler.dispose(); } this._overviewDomElement.removeChild(this._overviewViewportDomElement.domNode); - this._containerDomElement.removeChild(this._overviewDomElement); + if (this._renderOverviewRuler) { + this._containerDomElement.removeChild(this._overviewDomElement); + } this._containerDomElement.removeChild(this._originalDomNode); this._originalEditor.dispose(); @@ -678,7 +701,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return this._modifiedEditor; } - public updateOptions(newOptions: IDiffEditorOptions): void { + public updateOptions(newOptions: Readonly): void { // Handle side by side let renderSideBySideChanged = false; @@ -740,6 +763,16 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE // Update class name this._containerDomElement.className = DiffEditorWidget._getClassName(this._themeService.getColorTheme(), this._renderSideBySide); } + + // renderOverviewRuler + if (typeof newOptions.renderOverviewRuler !== 'undefined' && this._renderOverviewRuler !== newOptions.renderOverviewRuler) { + this._renderOverviewRuler = newOptions.renderOverviewRuler; + if (this._renderOverviewRuler) { + this._containerDomElement.appendChild(this._overviewDomElement); + } else { + this._containerDomElement.removeChild(this._overviewDomElement); + } + } } public getModel(): editorCommon.IDiffEditorModel { @@ -749,7 +782,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE }; } - public setModel(model: editorCommon.IDiffEditorModel): void { + public setModel(model: editorCommon.IDiffEditorModel | null): void { // Guard us against partial null model if (model && (!model.original || !model.modified)) { throw new Error(!model.original ? 'DiffEditorWidget.setModel: Original model is null' : 'DiffEditorWidget.setModel: Modified model is null'); @@ -911,7 +944,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } public restoreViewState(s: editorCommon.IDiffEditorViewState): void { - if (s.original && s.modified) { + if (s && s.original && s.modified) { const diffEditorState = s; this._originalEditor.restoreViewState(diffEditorState.original); this._modifiedEditor.restoreViewState(diffEditorState.modified); @@ -969,6 +1002,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } private _layoutOverviewRulers(): void { + if (!this._renderOverviewRuler) { + return; + } + if (!this._originalOverviewRuler || !this._modifiedOverviewRuler) { return; } @@ -1079,9 +1116,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } private _updateDecorations(): void { - if (!this._originalEditor.getModel() || !this._modifiedEditor.getModel() || !this._originalOverviewRuler || !this._modifiedOverviewRuler) { + if (!this._originalEditor.getModel() || !this._modifiedEditor.getModel()) { return; } + const lineChanges = (this._diffComputationResult ? this._diffComputationResult.changes : []); const foreignOriginal = this._originalEditorState.getForeignViewZones(this._originalEditor.getWhitespaces()); @@ -1098,25 +1136,24 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } } - private _adjustOptionsForSubEditor(options: editorBrowser.IDiffEditorConstructionOptions): editorBrowser.IDiffEditorConstructionOptions { - const clonedOptions: editorBrowser.IDiffEditorConstructionOptions = objects.deepClone(options || {}); + private _adjustOptionsForSubEditor(options: Readonly): editorBrowser.IEditorConstructionOptions { + const clonedOptions = { ...options }; clonedOptions.inDiffEditor = true; clonedOptions.automaticLayout = false; - clonedOptions.scrollbar = clonedOptions.scrollbar || {}; + // Clone scrollbar options before changing them + clonedOptions.scrollbar = { ...(clonedOptions.scrollbar || {}) }; clonedOptions.scrollbar.vertical = 'visible'; clonedOptions.folding = false; clonedOptions.codeLens = this._diffCodeLens; clonedOptions.fixedOverflowWidgets = true; - clonedOptions.overflowWidgetsDomNode = options.overflowWidgetsDomNode; // clonedOptions.lineDecorationsWidth = '2ch'; - if (!clonedOptions.minimap) { - clonedOptions.minimap = {}; - } + // Clone minimap options before changing them + clonedOptions.minimap = { ...(clonedOptions.minimap || {}) }; clonedOptions.minimap.enabled = false; return clonedOptions; } - private _adjustOptionsForLeftHandSide(options: editorBrowser.IDiffEditorConstructionOptions): editorBrowser.IEditorConstructionOptions { + private _adjustOptionsForLeftHandSide(options: Readonly): editorBrowser.IEditorConstructionOptions { const result = this._adjustOptionsForSubEditor(options); if (!this._renderSideBySide) { // never wrap hidden editor @@ -1126,16 +1163,28 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } result.readOnly = !this._originalIsEditable; result.extraEditorClassName = 'original-in-monaco-diff-editor'; - return result; + return { + ...result, + dimension: { + height: 0, + width: 0 + } + }; } - private _adjustOptionsForRightHandSide(options: editorBrowser.IDiffEditorConstructionOptions): editorBrowser.IEditorConstructionOptions { + private _adjustOptionsForRightHandSide(options: Readonly): editorBrowser.IEditorConstructionOptions { const result = this._adjustOptionsForSubEditor(options); result.wordWrapOverride1 = this._diffWordWrap; result.revealHorizontalRightPadding = EditorOptions.revealHorizontalRightPadding.defaultValue + DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH; result.scrollbar!.verticalHasArrows = false; result.extraEditorClassName = 'modified-in-monaco-diff-editor'; - return result; + return { + ...result, + dimension: { + height: 0, + width: 0 + } + }; } public doLayout(): void { @@ -1164,7 +1213,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._overviewViewportDomElement.setHeight(30); this._originalEditor.layout({ width: splitPoint, height: (height - reviewHeight) }); - this._modifiedEditor.layout({ width: width - splitPoint - DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH, height: (height - reviewHeight) }); + this._modifiedEditor.layout({ width: width - splitPoint - (this._renderOverviewRuler ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0), height: (height - reviewHeight) }); if (this._originalOverviewRuler || this._modifiedOverviewRuler) { this._layoutOverviewRulers(); @@ -1218,6 +1267,12 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return (this._elementSizeObserver.getHeight() - this._getReviewHeight()); }, + getOptions: () => { + return { + renderOverviewRuler: this._renderOverviewRuler + }; + }, + getContainerDomNode: () => { return this._containerDomElement; }, @@ -1347,6 +1402,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE interface IDataSource { getWidth(): number; getHeight(): number; + getOptions(): { renderOverviewRuler: boolean; }; getContainerDomNode(): HTMLElement; relayoutEditors(): void; @@ -1806,7 +1862,7 @@ class DiffEditorWidgetSideBySide extends DiffEditorWidgetStyle implements IVerti public layout(sashRatio: number | null = this._sashRatio): number { const w = this._dataSource.getWidth(); - const contentWidth = w - DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH; + const contentWidth = w - (this._dataSource.getOptions().renderOverviewRuler ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0); let sashPosition = Math.floor((sashRatio || 0.5) * contentWidth); const midPoint = Math.floor(0.5 * contentWidth); @@ -1839,7 +1895,7 @@ class DiffEditorWidgetSideBySide extends DiffEditorWidgetStyle implements IVerti private _onSashDrag(e: ISashEvent): void { const w = this._dataSource.getWidth(); - const contentWidth = w - DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH; + const contentWidth = w - (this._dataSource.getOptions().renderOverviewRuler ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0); const sashPosition = this.layout((this._startSashPosition! + (e.currentX - e.startX)) / contentWidth); this._sashRatio = sashPosition / contentWidth; @@ -2370,7 +2426,7 @@ class InlineViewZonesComputer extends ViewZonesComputer { const html = sb.build(); const trustedhtml = ttPolicy ? ttPolicy.createHTML(html) : html; - domNode.innerHTML = trustedhtml as unknown as string; + domNode.innerHTML = trustedhtml as string; viewZone.minWidthInPx = (maxCharsPerLine * typicalHalfwidthCharacterWidth); if (viewLineCounts) { diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index fd53314e..2fb0eaed 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -80,6 +80,8 @@ const diffReviewCloseIcon = registerIcon('diff-review-close', Codicon.close, nls export class DiffReview extends Disposable { + private static _ttPolicy = window.trustedTypes?.createPolicy('diffReview', { createHTML: value => value }); + private readonly _diffEditor: DiffEditorWidget; private _isVisible: boolean; public readonly shadow: FastDomNode; @@ -734,14 +736,18 @@ export class DiffReview extends Disposable { let lineContent: string; if (modifiedLine !== 0) { - cell.insertAdjacentHTML('beforeend', - this._renderLine(modifiedModel, modifiedOptions, modifiedModelOpts.tabSize, modifiedLine) - ); + let html: string | TrustedHTML = this._renderLine(modifiedModel, modifiedOptions, modifiedModelOpts.tabSize, modifiedLine); + if (DiffReview._ttPolicy) { + html = DiffReview._ttPolicy.createHTML(html as string); + } + cell.insertAdjacentHTML('beforeend', html as string); lineContent = modifiedModel.getLineContent(modifiedLine); } else { - cell.insertAdjacentHTML('beforeend', - this._renderLine(originalModel, originalOptions, originalModelOpts.tabSize, originalLine) - ); + let html: string | TrustedHTML = this._renderLine(originalModel, originalOptions, originalModelOpts.tabSize, originalLine); + if (DiffReview._ttPolicy) { + html = DiffReview._ttPolicy.createHTML(html as string); + } + cell.insertAdjacentHTML('beforeend', html as string); lineContent = originalModel.getLineContent(originalLine); } diff --git a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts index 5dd98c44..3836a4ec 100644 --- a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts +++ b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts @@ -82,7 +82,7 @@ export class EmbeddedDiffEditorWidget extends DiffEditorWidget { @IClipboardService clipboardService: IClipboardService, @IEditorProgressService editorProgressService: IEditorProgressService, ) { - super(domElement, parentEditor.getRawOptions(), clipboardService, editorWorkerService, contextKeyService, instantiationService, codeEditorService, themeService, notificationService, contextMenuService, editorProgressService); + super(domElement, parentEditor.getRawOptions(), {}, clipboardService, editorWorkerService, contextKeyService, instantiationService, codeEditorService, themeService, notificationService, contextMenuService, editorProgressService); this._parentEditor = parentEditor; this._overwriteOptions = options; diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index bf1d608f..c3805bf4 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -272,7 +272,7 @@ function migrateOptions(options: IEditorOptions): void { } } -function deepCloneAndMigrateOptions(_options: IEditorOptions): IEditorOptions { +function deepCloneAndMigrateOptions(_options: Readonly): IEditorOptions { const options = objects.deepClone(_options); migrateOptions(options); return options; @@ -298,7 +298,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC private _readOptions: RawEditorOptions; protected _validatedOptions: ValidatedEditorOptions; - constructor(isSimpleWidget: boolean, _options: IEditorOptions) { + constructor(isSimpleWidget: boolean, _options: Readonly) { super(); this.isSimpleWidget = isSimpleWidget; @@ -318,8 +318,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC public observeReferenceElement(dimension?: IDimension): void { } - public dispose(): void { - super.dispose(); + public updatePixelRatio(): void { } protected _recomputeOptions(): void { @@ -348,7 +347,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC private _computeInternalOptions(): ComputedEditorOptions { const partialEnv = this._getEnvConfiguration(); - const bareFontInfo = BareFontInfo.createFromValidatedSettings(this._validatedOptions, partialEnv.zoomLevel, this.isSimpleWidget); + const bareFontInfo = BareFontInfo.createFromValidatedSettings(this._validatedOptions, partialEnv.zoomLevel, partialEnv.pixelRatio, this.isSimpleWidget); const env: IEnvironmentalOptions = { memory: this._computeOptionsMemory, outerWidth: partialEnv.outerWidth, @@ -394,7 +393,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC return true; } - public updateOptions(_newOptions: IEditorOptions): void { + public updateOptions(_newOptions: Readonly): void { if (typeof _newOptions === 'undefined') { return; } diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 1c6b481c..bce85536 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -625,6 +625,10 @@ export interface IEditorOptions { * Controls strikethrough deprecated variables. */ showDeprecated?: boolean; + /** + * Control the behavior and rendering of the inline hints. + */ + inlineHints?: IEditorInlineHintsOptions; } /** @@ -677,6 +681,11 @@ export interface IDiffEditorOptions extends IEditorOptions { * Defaults to false */ isInEmbeddedEditor?: boolean; + /** + * Is the diff editor should render overview ruler + * Defaults to true + */ + renderOverviewRuler?: boolean; /** * Control the wrapping of the diff editor. */ @@ -2364,6 +2373,74 @@ class EditorLightbulb extends BaseEditorOption>; + +class EditorInlineHints extends BaseEditorOption { + + constructor() { + const defaults: EditorInlineHintsOptions = { enabled: true, fontSize: 0, fontFamily: EDITOR_FONT_DEFAULTS.fontFamily }; + super( + EditorOption.inlineHints, 'inlineHints', defaults, + { + 'editor.inlineHints.enabled': { + type: 'boolean', + default: defaults.enabled, + description: nls.localize('inlineHints.enable', "Enables the inline hints in the editor.") + }, + 'editor.inlineHints.fontSize': { + type: 'number', + default: defaults.fontSize, + description: nls.localize('inlineHints.fontSize', "Controls font size of inline hints in the editor. When set to `0`, the 90% of `#editor.fontSize#` is used.") + }, + 'editor.inlineHints.fontFamily': { + type: 'string', + default: defaults.fontFamily, + description: nls.localize('inlineHints.fontFamily', "Controls font family of inline hints in the editor.") + }, + } + ); + } + + public validate(_input: any): EditorInlineHintsOptions { + if (!_input || typeof _input !== 'object') { + return this.defaultValue; + } + const input = _input as IEditorInlineHintsOptions; + return { + enabled: boolean(input.enabled, this.defaultValue.enabled), + fontSize: EditorIntOption.clampedInt(input.fontSize, this.defaultValue.fontSize, 0, 100), + fontFamily: EditorStringOption.string(input.fontFamily, this.defaultValue.fontFamily) + }; + } +} + +//#endregion + //#region lineHeight class EditorLineHeight extends EditorIntOption { @@ -3273,7 +3350,7 @@ class EditorSuggest extends BaseEditorOption 0) { this._pushAutoClosedAction(autoClosedCharactersRanges, autoClosedEnclosingRanges); diff --git a/src/vs/editor/common/controller/cursorMoveOperations.ts b/src/vs/editor/common/controller/cursorMoveOperations.ts index 9229c2e3..ac505bdc 100644 --- a/src/vs/editor/common/controller/cursorMoveOperations.ts +++ b/src/vs/editor/common/controller/cursorMoveOperations.ts @@ -39,11 +39,11 @@ export class MoveOperations { public static leftPositionAtomicSoftTabs(model: ICursorSimpleModel, lineNumber: number, column: number, tabSize: number): Position { const minColumn = model.getLineMinColumn(lineNumber); const lineContent = model.getLineContent(lineNumber); - const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, column - minColumn, tabSize, Direction.Left); - if (newPosition === -1) { + const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, column - 1, tabSize, Direction.Left); + if (newPosition === -1 || newPosition + 1 < minColumn) { return this.leftPosition(model, lineNumber, column); } - return new Position(lineNumber, minColumn + newPosition); + return new Position(lineNumber, newPosition + 1); } public static left(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number): CursorPosition { @@ -81,13 +81,12 @@ export class MoveOperations { } public static rightPositionAtomicSoftTabs(model: ICursorSimpleModel, lineNumber: number, column: number, tabSize: number, indentSize: number): Position { - const minColumn = model.getLineMinColumn(lineNumber); const lineContent = model.getLineContent(lineNumber); - const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, column - minColumn, tabSize, Direction.Right); + const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, column - 1, tabSize, Direction.Right); if (newPosition === -1) { return this.rightPosition(model, lineNumber, column); } - return new Position(lineNumber, minColumn + newPosition); + return new Position(lineNumber, newPosition + 1); } public static right(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number): CursorPosition { diff --git a/src/vs/editor/common/controller/cursorTypeOperations.ts b/src/vs/editor/common/controller/cursorTypeOperations.ts index 75da0485..0c618026 100644 --- a/src/vs/editor/common/controller/cursorTypeOperations.ts +++ b/src/vs/editor/common/controller/cursorTypeOperations.ts @@ -351,13 +351,6 @@ export class TypeOperations { if (ir) { let oldEndViewColumn = CursorColumns.visibleColumnFromColumn2(config, model, range.getEndPosition()); const oldEndColumn = range.endColumn; - - let beforeText = '\n'; - if (indentation !== config.normalizeIndentation(ir.beforeEnter)) { - beforeText = config.normalizeIndentation(ir.beforeEnter) + lineText.substring(indentation.length, range.startColumn - 1) + '\n'; - range = new Range(range.startLineNumber, 1, range.endLineNumber, range.endColumn); - } - const newLineContent = model.getLineContent(range.endLineNumber); const firstNonWhitespace = strings.firstNonWhitespaceIndex(newLineContent); if (firstNonWhitespace >= 0) { @@ -367,7 +360,7 @@ export class TypeOperations { } if (keepPosition) { - return new ReplaceCommandWithoutChangingPosition(range, beforeText + config.normalizeIndentation(ir.afterEnter), true); + return new ReplaceCommandWithoutChangingPosition(range, '\n' + config.normalizeIndentation(ir.afterEnter), true); } else { let offset = 0; if (oldEndColumn <= firstNonWhitespace + 1) { @@ -376,7 +369,7 @@ export class TypeOperations { } offset = Math.min(oldEndViewColumn + 1 - config.normalizeIndentation(ir.afterEnter).length - 1, 0); } - return new ReplaceCommandWithOffsetCursorState(range, beforeText + config.normalizeIndentation(ir.afterEnter), 0, offset, true); + return new ReplaceCommandWithOffsetCursorState(range, '\n' + config.normalizeIndentation(ir.afterEnter), 0, offset, true); } } } diff --git a/src/vs/editor/common/diff/diffComputer.ts b/src/vs/editor/common/diff/diffComputer.ts index ebf85460..cac0acd9 100644 --- a/src/vs/editor/common/diff/diffComputer.ts +++ b/src/vs/editor/common/diff/diffComputer.ts @@ -313,6 +313,13 @@ export class DiffComputer { if (this.original.lines.length === 1 && this.original.lines[0].length === 0) { // empty original => fast path + if (this.modified.lines.length === 1 && this.modified.lines[0].length === 0) { + return { + quitEarly: false, + changes: [] + }; + } + return { quitEarly: false, changes: [{ diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 94c4a3c4..e2372db8 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -157,9 +157,10 @@ export interface IConfiguration extends IDisposable { setMaxLineNumber(maxLineNumber: number): void; setViewLineCount(viewLineCount: number): void; - updateOptions(newOptions: IEditorOptions): void; + updateOptions(newOptions: Readonly): void; getRawOptions(): IEditorOptions; observeReferenceElement(dimension?: IDimension): void; + updatePixelRatio(): void; setIsDominatedByLongLines(isDominatedByLongLines: boolean): void; } @@ -605,6 +606,7 @@ export interface IThemeDecorationRenderOptions { fontStyle?: string; fontWeight?: string; + fontSize?: string; textDecoration?: string; cursor?: string; color?: string | ThemeColor; @@ -629,13 +631,17 @@ export interface IContentDecorationRenderOptions { border?: string; borderColor?: string | ThemeColor; + borderRadius?: string; fontStyle?: string; fontWeight?: string; + fontSize?: string; + fontFamily?: string; textDecoration?: string; color?: string | ThemeColor; backgroundColor?: string | ThemeColor; margin?: string; + padding?: string; width?: string; height?: string; } diff --git a/src/vs/editor/common/editorContextKeys.ts b/src/vs/editor/common/editorContextKeys.ts index a3a3b3de..2f24e311 100644 --- a/src/vs/editor/common/editorContextKeys.ts +++ b/src/vs/editor/common/editorContextKeys.ts @@ -61,6 +61,7 @@ export namespace EditorContextKeys { export const hasReferenceProvider = new RawContextKey('editorHasReferenceProvider', false); export const hasRenameProvider = new RawContextKey('editorHasRenameProvider', false); export const hasSignatureHelpProvider = new RawContextKey('editorHasSignatureHelpProvider', false); + export const hasInlineHintsProvider = new RawContextKey('editorHasInlineHintsProvider', false); // -- mode context keys: formatting export const hasDocumentFormattingProvider = new RawContextKey('editorHasDocumentFormattingProvider', false); diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 8f4d53a8..3f1a239d 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -600,12 +600,6 @@ export interface ITextModel { */ setValue(newValue: string): void; - /** - * Replace the entire text buffer value contained in this model. - * @internal - */ - setValueFromTextBuffer(newValue: ITextBuffer): void; - /** * Get the text stored in this model. * @param eol The end of line character preference. Defaults to `EndOfLinePreference.TextDefined`. @@ -1276,7 +1270,7 @@ export interface ITextBufferBuilder { * @internal */ export interface ITextBufferFactory { - create(defaultEOL: DefaultEndOfLine): ITextBuffer; + create(defaultEOL: DefaultEndOfLine): { textBuffer: ITextBuffer; disposable: IDisposable; }; getFirstLineText(lengthLimit: number): string; } diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts index d134517b..b65b87a0 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CharCode } from 'vs/base/common/charCode'; +import { IDisposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import { DefaultEndOfLine, ITextBuffer, ITextBufferBuilder, ITextBufferFactory } from 'vs/editor/common/model'; import { StringBuffer, createLineStarts, createLineStartsFast } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase'; @@ -38,7 +39,7 @@ export class PieceTreeTextBufferFactory implements ITextBufferFactory { return '\n'; } - public create(defaultEOL: DefaultEndOfLine): ITextBuffer { + public create(defaultEOL: DefaultEndOfLine): { textBuffer: ITextBuffer; disposable: IDisposable; } { const eol = this._getEOL(defaultEOL); let chunks = this._chunks; @@ -54,7 +55,8 @@ export class PieceTreeTextBufferFactory implements ITextBufferFactory { } } - return new PieceTreeTextBuffer(chunks, this._bom, eol, this._containsRTL, this._containsUnusualLineTerminators, this._isBasicASCII, this._normalizeEOL); + const textBuffer = new PieceTreeTextBuffer(chunks, this._bom, eol, this._containsRTL, this._containsUnusualLineTerminators, this._isBasicASCII, this._normalizeEOL); + return { textBuffer: textBuffer, disposable: textBuffer }; } public getFirstLineText(lengthLimit: number): string { diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 35dd4664..aab72a48 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -37,6 +37,7 @@ import { EditorTheme } from 'vs/editor/common/view/viewContext'; import { IUndoRedoService, ResourceEditStackSnapshot } from 'vs/platform/undoRedo/common/undoRedo'; import { TextChange } from 'vs/editor/common/model/textChange'; import { Constants } from 'vs/base/common/uint'; +import { PieceTreeTextBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer'; function createTextBufferBuilder() { return new PieceTreeTextBufferBuilder(); @@ -106,7 +107,7 @@ export function createTextBufferFactoryFromSnapshot(snapshot: model.ITextSnapsho return builder.finish(); } -export function createTextBuffer(value: string | model.ITextBufferFactory, defaultEOL: model.DefaultEndOfLine): model.ITextBuffer { +export function createTextBuffer(value: string | model.ITextBufferFactory, defaultEOL: model.DefaultEndOfLine): { textBuffer: model.ITextBuffer; disposable: IDisposable; } { const factory = (typeof value === 'string' ? createTextBufferFactory(value) : value); return factory.create(defaultEOL); } @@ -268,6 +269,7 @@ export class TextModel extends Disposable implements model.ITextModel { private readonly _undoRedoService: IUndoRedoService; private _attachedEditorCount: number; private _buffer: model.ITextBuffer; + private _bufferDisposable: IDisposable; private _options: model.TextModelResolvedOptions; private _isDisposed: boolean; @@ -328,7 +330,9 @@ export class TextModel extends Disposable implements model.ITextModel { this._undoRedoService = undoRedoService; this._attachedEditorCount = 0; - this._buffer = createTextBuffer(source, creationOptions.defaultEOL); + const { textBuffer, disposable } = createTextBuffer(source, creationOptions.defaultEOL); + this._buffer = textBuffer; + this._bufferDisposable = disposable; this._options = TextModel.resolveOptions(this._buffer, creationOptions); @@ -386,10 +390,13 @@ export class TextModel extends Disposable implements model.ITextModel { this._tokenization.dispose(); this._isDisposed = true; super.dispose(); + this._bufferDisposable.dispose(); this._isDisposing = false; // Manually release reference to previous text buffer to avoid large leaks // in case someone leaks a TextModel reference - this._buffer = createTextBuffer('', this._options.defaultEOL); + const emptyDisposedTextBuffer = new PieceTreeTextBuffer([], '', '\n', false, false, true, true); + emptyDisposedTextBuffer.dispose(); + this._buffer = emptyDisposedTextBuffer; } private _assertNotDisposed(): void { @@ -423,8 +430,8 @@ export class TextModel extends Disposable implements model.ITextModel { return; } - const textBuffer = createTextBuffer(value, this._options.defaultEOL); - this.setValueFromTextBuffer(textBuffer); + const { textBuffer, disposable } = createTextBuffer(value, this._options.defaultEOL); + this._setValueFromTextBuffer(textBuffer, disposable); } private _createContentChanged2(range: Range, rangeOffset: number, rangeLength: number, text: string, isUndoing: boolean, isRedoing: boolean, isFlush: boolean): IModelContentChangedEvent { @@ -443,18 +450,16 @@ export class TextModel extends Disposable implements model.ITextModel { }; } - public setValueFromTextBuffer(textBuffer: model.ITextBuffer): void { + private _setValueFromTextBuffer(textBuffer: model.ITextBuffer, textBufferDisposable: IDisposable): void { this._assertNotDisposed(); - if (textBuffer === null) { - // There's nothing to do - return; - } const oldFullModelRange = this.getFullModelRange(); const oldModelValueLength = this.getValueLengthInRange(oldFullModelRange); const endLineNumber = this.getLineCount(); const endColumn = this.getLineMaxColumn(endLineNumber); this._buffer = textBuffer; + this._bufferDisposable.dispose(); + this._bufferDisposable = textBufferDisposable; this._increaseVersionId(); // Flush all tokens diff --git a/src/vs/editor/common/model/textModelTokens.ts b/src/vs/editor/common/model/textModelTokens.ts index e127f50e..3cf7bc9a 100644 --- a/src/vs/editor/common/model/textModelTokens.ts +++ b/src/vs/editor/common/model/textModelTokens.ts @@ -359,7 +359,7 @@ export class TextModelTokenization extends Disposable { const text = this._textModel.getLineContent(lineIndex + 1); const lineStartState = this._tokenizationStateStore.getBeginState(lineIndex); - const r = safeTokenize(languageIdentifier, this._tokenizationSupport, text, lineStartState!); + const r = safeTokenize(languageIdentifier, this._tokenizationSupport, text, true, lineStartState!); builder.add(lineIndex + 1, r.tokens); this._tokenizationStateStore.setEndState(linesLength, lineIndex, r.endState); lineIndex = this._tokenizationStateStore.invalidLineStartIndex - 1; // -1 because the outer loop increments it @@ -410,13 +410,13 @@ export class TextModelTokenization extends Disposable { const languageIdentifier = this._textModel.getLanguageIdentifier(); let state = initialState; for (let i = fakeLines.length - 1; i >= 0; i--) { - let r = safeTokenize(languageIdentifier, this._tokenizationSupport, fakeLines[i], state); + let r = safeTokenize(languageIdentifier, this._tokenizationSupport, fakeLines[i], false, state); state = r.endState; } for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { let text = this._textModel.getLineContent(lineNumber); - let r = safeTokenize(languageIdentifier, this._tokenizationSupport, text, state); + let r = safeTokenize(languageIdentifier, this._tokenizationSupport, text, true, state); builder.add(lineNumber, r.tokens); this._tokenizationStateStore.setFakeTokens(lineNumber - 1); state = r.endState; @@ -443,12 +443,12 @@ function initializeTokenization(textModel: TextModel): [ITokenizationSupport | n return [tokenizationSupport, initialState]; } -function safeTokenize(languageIdentifier: LanguageIdentifier, tokenizationSupport: ITokenizationSupport | null, text: string, state: IState): TokenizationResult2 { +function safeTokenize(languageIdentifier: LanguageIdentifier, tokenizationSupport: ITokenizationSupport | null, text: string, hasEOL: boolean, state: IState): TokenizationResult2 { let r: TokenizationResult2 | null = null; if (tokenizationSupport) { try { - r = tokenizationSupport.tokenize2(text, state.clone(), 0); + r = tokenizationSupport.tokenize2(text, hasEOL, state.clone(), 0); } catch (e) { onUnexpectedError(e); } diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index fe0d7335..204a4503 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -211,9 +211,9 @@ export interface ITokenizationSupport { getInitialState(): IState; // add offsetDelta to each of the returned indices - tokenize(line: string, state: IState, offsetDelta: number): TokenizationResult; + tokenize(line: string, hasEOL: boolean, state: IState, offsetDelta: number): TokenizationResult; - tokenize2(line: string, state: IState, offsetDelta: number): TokenizationResult2; + tokenize2(line: string, hasEOL: boolean, state: IState, offsetDelta: number): TokenizationResult2; } /** @@ -1659,6 +1659,19 @@ export interface CodeLensProvider { resolveCodeLens?(model: model.ITextModel, codeLens: CodeLens, token: CancellationToken): ProviderResult; } +export interface InlineHint { + text: string; + range: IRange; + description?: string | IMarkdownString; + whitespaceBefore?: boolean; + whitespaceAfter?: boolean; +} + +export interface InlineHintsProvider { + onDidChangeInlineHints?: Event | undefined; + provideInlineHints(model: model.ITextModel, range: Range, token: CancellationToken): ProviderResult; +} + export interface SemanticTokensLegend { readonly tokenTypes: string[]; readonly tokenModifiers: string[]; @@ -1764,6 +1777,11 @@ export const TypeDefinitionProviderRegistry = new LanguageFeatureRegistry(); +/** + * @internal + */ +export const InlineHintsProviderRegistry = new LanguageFeatureRegistry(); + /** * @internal */ @@ -1876,3 +1894,14 @@ export interface ITokenizationRegistry { * @internal */ export const TokenizationRegistry = new TokenizationRegistryImpl(); + + +/** + * @internal + */ +export enum ExternalUriOpenerPriority { + None = 0, + Option = 1, + Default = 2, + Preferred = 3, +} diff --git a/src/vs/editor/common/modes/languageConfiguration.ts b/src/vs/editor/common/modes/languageConfiguration.ts index a383c57e..3542fa74 100644 --- a/src/vs/editor/common/modes/languageConfiguration.ts +++ b/src/vs/editor/common/modes/languageConfiguration.ts @@ -150,7 +150,7 @@ export interface OnEnterRule { /** * This rule will only execute if the text above the this line matches this regular expression. */ - oneLineAboveText?: RegExp; + previousLineText?: RegExp; /** * The action to execute. */ diff --git a/src/vs/editor/common/modes/languageConfigurationRegistry.ts b/src/vs/editor/common/modes/languageConfigurationRegistry.ts index ccd20e37..6074e34c 100644 --- a/src/vs/editor/common/modes/languageConfigurationRegistry.ts +++ b/src/vs/editor/common/modes/languageConfigurationRegistry.ts @@ -101,11 +101,11 @@ export class RichEditSupport { return this._electricCharacter; } - public onEnter(autoIndent: EditorAutoIndentStrategy, oneLineAboveText: string, beforeEnterText: string, afterEnterText: string): EnterAction | null { + public onEnter(autoIndent: EditorAutoIndentStrategy, previousLineText: string, beforeEnterText: string, afterEnterText: string): EnterAction | null { if (!this._onEnterSupport) { return null; } - return this._onEnterSupport.onEnter(autoIndent, oneLineAboveText, beforeEnterText, afterEnterText); + return this._onEnterSupport.onEnter(autoIndent, previousLineText, beforeEnterText, afterEnterText); } private static _mergeConf(prev: LanguageConfiguration | null, current: LanguageConfiguration): LanguageConfiguration { @@ -700,17 +700,17 @@ export class LanguageConfigurationRegistryImpl { afterEnterText = endScopedLineTokens.getLineContent().substr(range.endColumn - 1 - scopedLineTokens.firstCharOffset); } - let oneLineAboveText = ''; + let previousLineText = ''; if (range.startLineNumber > 1 && scopedLineTokens.firstCharOffset === 0) { // This is not the first line and the entire line belongs to this mode const oneLineAboveScopedLineTokens = this.getScopedLineTokens(model, range.startLineNumber - 1); if (oneLineAboveScopedLineTokens.languageId === scopedLineTokens.languageId) { // The line above ends with text belonging to the same mode - oneLineAboveText = oneLineAboveScopedLineTokens.getLineContent(); + previousLineText = oneLineAboveScopedLineTokens.getLineContent(); } } - const enterResult = richEditSupport.onEnter(autoIndent, oneLineAboveText, beforeEnterText, afterEnterText); + const enterResult = richEditSupport.onEnter(autoIndent, previousLineText, beforeEnterText, afterEnterText); if (!enterResult) { return null; } @@ -729,6 +729,8 @@ export class LanguageConfigurationRegistryImpl { } else { appendText = ''; } + } else if (indentAction === IndentAction.Indent) { + appendText = '\t' + appendText; } let indentation = this.getIndentationAtPosition(model, range.startLineNumber, range.startColumn); diff --git a/src/vs/editor/common/modes/modesRegistry.ts b/src/vs/editor/common/modes/modesRegistry.ts index c2ef6338..ffc97c24 100644 --- a/src/vs/editor/common/modes/modesRegistry.ts +++ b/src/vs/editor/common/modes/modesRegistry.ts @@ -58,11 +58,12 @@ export const ModesRegistry = new EditorModesRegistry(); Registry.add(Extensions.ModesRegistry, ModesRegistry); export const PLAINTEXT_MODE_ID = 'plaintext'; +export const PLAINTEXT_EXTENSION = '.txt'; export const PLAINTEXT_LANGUAGE_IDENTIFIER = new LanguageIdentifier(PLAINTEXT_MODE_ID, LanguageId.PlainText); ModesRegistry.registerLanguage({ id: PLAINTEXT_MODE_ID, - extensions: ['.txt'], + extensions: [PLAINTEXT_EXTENSION], aliases: [nls.localize('plainText.alias', "Plain Text"), 'text'], mimetypes: ['text/plain'] }); diff --git a/src/vs/editor/common/modes/supports/onEnter.ts b/src/vs/editor/common/modes/supports/onEnter.ts index f42cc4a4..c03b5243 100644 --- a/src/vs/editor/common/modes/supports/onEnter.ts +++ b/src/vs/editor/common/modes/supports/onEnter.ts @@ -49,7 +49,7 @@ export class OnEnterSupport { this._regExpRules = opts.onEnterRules || []; } - public onEnter(autoIndent: EditorAutoIndentStrategy, oneLineAboveText: string, beforeEnterText: string, afterEnterText: string): EnterAction | null { + public onEnter(autoIndent: EditorAutoIndentStrategy, previousLineText: string, beforeEnterText: string, afterEnterText: string): EnterAction | null { // (1): `regExpRules` if (autoIndent >= EditorAutoIndentStrategy.Advanced) { for (let i = 0, len = this._regExpRules.length; i < len; i++) { @@ -61,8 +61,8 @@ export class OnEnterSupport { reg: rule.afterText, text: afterEnterText }, { - reg: rule.oneLineAboveText, - text: oneLineAboveText + reg: rule.previousLineText, + text: previousLineText }].every((obj): boolean => { return obj.reg ? obj.reg.test(obj.text) : true; }); diff --git a/src/vs/editor/common/modes/textToHtmlTokenizer.ts b/src/vs/editor/common/modes/textToHtmlTokenizer.ts index c4866641..a0eb66d2 100644 --- a/src/vs/editor/common/modes/textToHtmlTokenizer.ts +++ b/src/vs/editor/common/modes/textToHtmlTokenizer.ts @@ -12,12 +12,12 @@ import { NULL_STATE, nullTokenize2 } from 'vs/editor/common/modes/nullMode'; export interface IReducedTokenizationSupport { getInitialState(): IState; - tokenize2(line: string, state: IState, offsetDelta: number): TokenizationResult2; + tokenize2(line: string, hasEOL: boolean, state: IState, offsetDelta: number): TokenizationResult2; } const fallback: IReducedTokenizationSupport = { getInitialState: () => NULL_STATE, - tokenize2: (buffer: string, state: IState, deltaOffset: number) => nullTokenize2(LanguageId.Null, buffer, state, deltaOffset) + tokenize2: (buffer: string, hasEOL: boolean, state: IState, deltaOffset: number) => nullTokenize2(LanguageId.Null, buffer, state, deltaOffset) }; export function tokenizeToString(text: string, tokenizationSupport: IReducedTokenizationSupport = fallback): string { @@ -110,7 +110,7 @@ function _tokenizeToString(text: string, tokenizationSupport: IReducedTokenizati result += `
    `; } - let tokenizationResult = tokenizationSupport.tokenize2(line, currentState, 0); + let tokenizationResult = tokenizationSupport.tokenize2(line, true, currentState, 0); LineTokens.convertToEndOffset(tokenizationResult.tokens, line.length); let lineTokens = new LineTokens(tokenizationResult.tokens, line); let viewLineTokens = lineTokens.inflate(); diff --git a/src/vs/editor/common/services/getSemanticTokens.ts b/src/vs/editor/common/services/getSemanticTokens.ts new file mode 100644 index 00000000..b0317a83 --- /dev/null +++ b/src/vs/editor/common/services/getSemanticTokens.ts @@ -0,0 +1,160 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { URI } from 'vs/base/common/uri'; +import { ITextModel } from 'vs/editor/common/model'; +import { DocumentSemanticTokensProviderRegistry, DocumentSemanticTokensProvider, SemanticTokens, SemanticTokensEdits, SemanticTokensLegend, DocumentRangeSemanticTokensProviderRegistry, DocumentRangeSemanticTokensProvider } from 'vs/editor/common/modes'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; +import { assertType } from 'vs/base/common/types'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { encodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; +import { Range } from 'vs/editor/common/core/range'; + +export function isSemanticTokens(v: SemanticTokens | SemanticTokensEdits): v is SemanticTokens { + return v && !!((v).data); +} + +export function isSemanticTokensEdits(v: SemanticTokens | SemanticTokensEdits): v is SemanticTokensEdits { + return v && Array.isArray((v).edits); +} + +export interface IDocumentSemanticTokensResult { + provider: DocumentSemanticTokensProvider; + request: Promise; +} + +export function getDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): IDocumentSemanticTokensResult | null { + const provider = _getDocumentSemanticTokensProvider(model); + if (!provider) { + return null; + } + return { + provider: provider, + request: Promise.resolve(provider.provideDocumentSemanticTokens(model, lastResultId, token)) + }; +} + +function _getDocumentSemanticTokensProvider(model: ITextModel): DocumentSemanticTokensProvider | null { + const result = DocumentSemanticTokensProviderRegistry.ordered(model); + return (result.length > 0 ? result[0] : null); +} + +export function getDocumentRangeSemanticTokensProvider(model: ITextModel): DocumentRangeSemanticTokensProvider | null { + const result = DocumentRangeSemanticTokensProviderRegistry.ordered(model); + return (result.length > 0 ? result[0] : null); +} + +CommandsRegistry.registerCommand('_provideDocumentSemanticTokensLegend', async (accessor, ...args): Promise => { + const [uri] = args; + assertType(uri instanceof URI); + + const model = accessor.get(IModelService).getModel(uri); + if (!model) { + return undefined; + } + + const provider = _getDocumentSemanticTokensProvider(model); + if (!provider) { + // there is no provider => fall back to a document range semantic tokens provider + return accessor.get(ICommandService).executeCommand('_provideDocumentRangeSemanticTokensLegend', uri); + } + + return provider.getLegend(); +}); + +CommandsRegistry.registerCommand('_provideDocumentSemanticTokens', async (accessor, ...args): Promise => { + const [uri] = args; + assertType(uri instanceof URI); + + const model = accessor.get(IModelService).getModel(uri); + if (!model) { + return undefined; + } + + const r = getDocumentSemanticTokens(model, null, CancellationToken.None); + if (!r) { + // there is no provider => fall back to a document range semantic tokens provider + return accessor.get(ICommandService).executeCommand('_provideDocumentRangeSemanticTokens', uri, model.getFullModelRange()); + } + + const { provider, request } = r; + + let result: SemanticTokens | SemanticTokensEdits | null | undefined; + try { + result = await request; + } catch (err) { + onUnexpectedExternalError(err); + return undefined; + } + + if (!result || !isSemanticTokens(result)) { + return undefined; + } + + const buff = encodeSemanticTokensDto({ + id: 0, + type: 'full', + data: result.data + }); + if (result.resultId) { + provider.releaseDocumentSemanticTokens(result.resultId); + } + return buff; +}); + +CommandsRegistry.registerCommand('_provideDocumentRangeSemanticTokensLegend', async (accessor, ...args): Promise => { + const [uri] = args; + assertType(uri instanceof URI); + + const model = accessor.get(IModelService).getModel(uri); + if (!model) { + return undefined; + } + + const provider = getDocumentRangeSemanticTokensProvider(model); + if (!provider) { + return undefined; + } + + return provider.getLegend(); +}); + +CommandsRegistry.registerCommand('_provideDocumentRangeSemanticTokens', async (accessor, ...args): Promise => { + const [uri, range] = args; + assertType(uri instanceof URI); + assertType(Range.isIRange(range)); + + const model = accessor.get(IModelService).getModel(uri); + if (!model) { + return undefined; + } + + const provider = getDocumentRangeSemanticTokensProvider(model); + if (!provider) { + // there is no provider + return undefined; + } + + let result: SemanticTokens | null | undefined; + try { + result = await provider.provideDocumentRangeSemanticTokens(model, Range.lift(range), CancellationToken.None); + } catch (err) { + onUnexpectedExternalError(err); + return undefined; + } + + if (!result || !isSemanticTokens(result)) { + return undefined; + } + + return encodeSemanticTokensDto({ + id: 0, + type: 'full', + data: result.data + }); +}); diff --git a/src/vs/editor/common/services/markerDecorationsServiceImpl.ts b/src/vs/editor/common/services/markerDecorationsServiceImpl.ts index eb2d6795..a80302dd 100644 --- a/src/vs/editor/common/services/markerDecorationsServiceImpl.ts +++ b/src/vs/editor/common/services/markerDecorationsServiceImpl.ts @@ -87,13 +87,13 @@ export class MarkerDecorationsService extends Disposable implements IMarkerDecor this._markerDecorations.clear(); } - getMarker(model: ITextModel, decoration: IModelDecoration): IMarker | null { - const markerDecorations = this._markerDecorations.get(MODEL_ID(model.uri)); + getMarker(uri: URI, decoration: IModelDecoration): IMarker | null { + const markerDecorations = this._markerDecorations.get(MODEL_ID(uri)); return markerDecorations ? (markerDecorations.getMarker(decoration) || null) : null; } - getLiveMarkers(model: ITextModel): [Range, IMarker][] { - const markerDecorations = this._markerDecorations.get(MODEL_ID(model.uri)); + getLiveMarkers(uri: URI): [Range, IMarker][] { + const markerDecorations = this._markerDecorations.get(MODEL_ID(uri)); return markerDecorations ? markerDecorations.getMarkers() : []; } diff --git a/src/vs/editor/common/services/markersDecorationService.ts b/src/vs/editor/common/services/markersDecorationService.ts index 745260c8..221f7237 100644 --- a/src/vs/editor/common/services/markersDecorationService.ts +++ b/src/vs/editor/common/services/markersDecorationService.ts @@ -8,6 +8,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IMarker } from 'vs/platform/markers/common/markers'; import { Event } from 'vs/base/common/event'; import { Range } from 'vs/editor/common/core/range'; +import { URI } from 'vs/base/common/uri'; export const IMarkerDecorationsService = createDecorator('markerDecorationsService'); @@ -16,7 +17,7 @@ export interface IMarkerDecorationsService { onDidChangeMarker: Event; - getMarker(model: ITextModel, decoration: IModelDecoration): IMarker | null; + getMarker(uri: URI, decoration: IModelDecoration): IMarker | null; - getLiveMarkers(model: ITextModel): [Range, IMarker][]; + getLiveMarkers(uri: URI): [Range, IMarker][]; } diff --git a/src/vs/editor/common/services/modeService.ts b/src/vs/editor/common/services/modeService.ts index 2739d652..e52fbf61 100644 --- a/src/vs/editor/common/services/modeService.ts +++ b/src/vs/editor/common/services/modeService.ts @@ -50,7 +50,7 @@ export interface IModeService { // --- instantiation create(commaSeparatedMimetypesOrCommaSeparatedIds: string | undefined): ILanguageSelection; createByLanguageName(languageName: string): ILanguageSelection; - createByFilepathOrFirstLine(rsource: URI | null, firstLine?: string): ILanguageSelection; + createByFilepathOrFirstLine(resource: URI | null, firstLine?: string): ILanguageSelection; triggerMode(commaSeparatedMimetypesOrCommaSeparatedIds: string): void; } diff --git a/src/vs/editor/common/services/modeServiceImpl.ts b/src/vs/editor/common/services/modeServiceImpl.ts index 6b2fd6f8..f5a6eba1 100644 --- a/src/vs/editor/common/services/modeServiceImpl.ts +++ b/src/vs/editor/common/services/modeServiceImpl.ts @@ -40,23 +40,24 @@ class LanguageSelection extends Disposable implements ILanguageSelection { } } -export class ModeServiceImpl implements IModeService { +export class ModeServiceImpl extends Disposable implements IModeService { public _serviceBrand: undefined; private readonly _instantiatedModes: { [modeId: string]: IMode; }; private readonly _registry: LanguagesRegistry; - private readonly _onDidCreateMode = new Emitter(); + private readonly _onDidCreateMode = this._register(new Emitter()); public readonly onDidCreateMode: Event = this._onDidCreateMode.event; - protected readonly _onLanguagesMaybeChanged = new Emitter(); + protected readonly _onLanguagesMaybeChanged = this._register(new Emitter()); public readonly onLanguagesMaybeChanged: Event = this._onLanguagesMaybeChanged.event; constructor(warnOnOverwrite = false) { + super(); this._instantiatedModes = {}; - this._registry = new LanguagesRegistry(true, warnOnOverwrite); - this._registry.onDidChange(() => this._onLanguagesMaybeChanged.fire()); + this._registry = this._register(new LanguagesRegistry(true, warnOnOverwrite)); + this._register(this._registry.onDidChange(() => this._onLanguagesMaybeChanged.fire())); } protected _onReady(): Promise { diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 86672768..15824fd7 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -29,6 +29,7 @@ import { StringSHA1 } from 'vs/base/common/hash'; import { EditStackElement, isEditStackElement } from 'vs/editor/common/model/editStack'; import { Schemas } from 'vs/base/common/network'; import { SemanticTokensProviderStyling, toMultilineTokens2 } from 'vs/editor/common/services/semanticTokensProviderStyling'; +import { getDocumentSemanticTokens, isSemanticTokens, isSemanticTokensEdits } from 'vs/editor/common/services/getSemanticTokens'; export interface IEditorSemanticHighlightingOptions { enabled: true | false | 'configuredByTheme'; @@ -412,10 +413,11 @@ export class ModelServiceImpl extends Disposable implements IModelService { public updateModel(model: ITextModel, value: string | ITextBufferFactory): void { const options = this.getCreationOptions(model.getLanguageIdentifier().language, model.uri, model.isForSimpleWidget); - const textBuffer = createTextBuffer(value, options.defaultEOL); + const { textBuffer, disposable } = createTextBuffer(value, options.defaultEOL); // Return early if the text is already set in that form if (model.equalsTextBuffer(textBuffer)) { + disposable.dispose(); return; } @@ -428,6 +430,7 @@ export class ModelServiceImpl extends Disposable implements IModelService { () => [] ); model.pushStackElement(); + disposable.dispose(); } private static _commonPrefix(a: ILineSequence, aLen: number, aDelta: number, b: ILineSequence, bLen: number, bDelta: number): number { @@ -513,58 +516,6 @@ export class ModelServiceImpl extends Disposable implements IModelService { if (!modelData) { return; } - const model = modelData.model; - const sharesUndoRedoStack = (this._undoRedoService.getUriComparisonKey(model.uri) !== model.uri.toString()); - let maintainUndoRedoStack = false; - let heapSize = 0; - if (sharesUndoRedoStack || (this._shouldRestoreUndoStack() && schemaShouldMaintainUndoRedoElements(resource))) { - const elements = this._undoRedoService.getElements(resource); - if (elements.past.length > 0 || elements.future.length > 0) { - for (const element of elements.past) { - if (isEditStackElement(element) && element.matchesResource(resource)) { - maintainUndoRedoStack = true; - heapSize += element.heapSize(resource); - element.setModel(resource); // remove reference from text buffer instance - } - } - for (const element of elements.future) { - if (isEditStackElement(element) && element.matchesResource(resource)) { - maintainUndoRedoStack = true; - heapSize += element.heapSize(resource); - element.setModel(resource); // remove reference from text buffer instance - } - } - } - } - - if (!maintainUndoRedoStack) { - if (!sharesUndoRedoStack) { - const initialUndoRedoSnapshot = modelData.model.getInitialUndoRedoSnapshot(); - if (initialUndoRedoSnapshot !== null) { - this._undoRedoService.restoreSnapshot(initialUndoRedoSnapshot); - } - } - modelData.model.dispose(); - return; - } - - const maxMemory = ModelServiceImpl.MAX_MEMORY_FOR_CLOSED_FILES_UNDO_STACK; - if (!sharesUndoRedoStack && heapSize > maxMemory) { - // the undo stack for this file would never fit in the configured memory, so don't bother with it. - const initialUndoRedoSnapshot = modelData.model.getInitialUndoRedoSnapshot(); - if (initialUndoRedoSnapshot !== null) { - this._undoRedoService.restoreSnapshot(initialUndoRedoSnapshot); - } - modelData.model.dispose(); - return; - } - - this._ensureDisposedModelsHeapSize(maxMemory - heapSize); - - // We only invalidate the elements, but they remain in the undo-redo service. - this._undoRedoService.setElementsValidFlag(resource, false, (element) => (isEditStackElement(element) && element.matchesResource(resource))); - this._insertDisposedModel(new DisposedModelInfo(resource, modelData.model.getInitialUndoRedoSnapshot(), Date.now(), sharesUndoRedoStack, heapSize, computeModelSha1(model), model.getVersionId(), model.getAlternativeVersionId())); - modelData.model.dispose(); } @@ -599,6 +550,50 @@ export class ModelServiceImpl extends Disposable implements IModelService { const modelId = MODEL_ID(model.uri); const modelData = this._models[modelId]; + const sharesUndoRedoStack = (this._undoRedoService.getUriComparisonKey(model.uri) !== model.uri.toString()); + let maintainUndoRedoStack = false; + let heapSize = 0; + if (sharesUndoRedoStack || (this._shouldRestoreUndoStack() && schemaShouldMaintainUndoRedoElements(model.uri))) { + const elements = this._undoRedoService.getElements(model.uri); + if (elements.past.length > 0 || elements.future.length > 0) { + for (const element of elements.past) { + if (isEditStackElement(element) && element.matchesResource(model.uri)) { + maintainUndoRedoStack = true; + heapSize += element.heapSize(model.uri); + element.setModel(model.uri); // remove reference from text buffer instance + } + } + for (const element of elements.future) { + if (isEditStackElement(element) && element.matchesResource(model.uri)) { + maintainUndoRedoStack = true; + heapSize += element.heapSize(model.uri); + element.setModel(model.uri); // remove reference from text buffer instance + } + } + } + } + + const maxMemory = ModelServiceImpl.MAX_MEMORY_FOR_CLOSED_FILES_UNDO_STACK; + if (!maintainUndoRedoStack) { + if (!sharesUndoRedoStack) { + const initialUndoRedoSnapshot = modelData.model.getInitialUndoRedoSnapshot(); + if (initialUndoRedoSnapshot !== null) { + this._undoRedoService.restoreSnapshot(initialUndoRedoSnapshot); + } + } + } else if (!sharesUndoRedoStack && heapSize > maxMemory) { + // the undo stack for this file would never fit in the configured memory, so don't bother with it. + const initialUndoRedoSnapshot = modelData.model.getInitialUndoRedoSnapshot(); + if (initialUndoRedoSnapshot !== null) { + this._undoRedoService.restoreSnapshot(initialUndoRedoSnapshot); + } + } else { + this._ensureDisposedModelsHeapSize(maxMemory - heapSize); + // We only invalidate the elements, but they remain in the undo-redo service. + this._undoRedoService.setElementsValidFlag(model.uri, false, (element) => (isEditStackElement(element) && element.matchesResource(model.uri))); + this._insertDisposedModel(new DisposedModelInfo(model.uri, modelData.model.getInitialUndoRedoSnapshot(), Date.now(), sharesUndoRedoStack, heapSize, computeModelSha1(model), model.getVersionId(), model.getAlternativeVersionId())); + } + delete this._models[modelId]; modelData.dispose(); @@ -718,7 +713,9 @@ class SemanticTokensResponse { } } -class ModelSemanticColoring extends Disposable { +export class ModelSemanticColoring extends Disposable { + + public static FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY = 300; private _isDisposed: boolean; private readonly _model: ITextModel; @@ -734,7 +731,7 @@ class ModelSemanticColoring extends Disposable { this._isDisposed = false; this._model = model; this._semanticStyling = stylingProvider; - this._fetchDocumentSemanticTokens = this._register(new RunOnceScheduler(() => this._fetchDocumentSemanticTokensNow(), 300)); + this._fetchDocumentSemanticTokens = this._register(new RunOnceScheduler(() => this._fetchDocumentSemanticTokensNow(), ModelSemanticColoring.FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY)); this._currentDocumentResponse = null; this._currentDocumentRequestCancellationTokenSource = null; this._documentProvidersChangeListeners = []; @@ -788,15 +785,21 @@ class ModelSemanticColoring extends Disposable { // there is already a request running, let it finish... return; } - const provider = this._getSemanticColoringProvider(); - if (!provider) { + + const cancellationTokenSource = new CancellationTokenSource(); + const lastResultId = this._currentDocumentResponse ? this._currentDocumentResponse.resultId || null : null; + const r = getDocumentSemanticTokens(this._model, lastResultId, cancellationTokenSource.token); + if (!r) { + // there is no provider if (this._currentDocumentResponse) { // there are semantic tokens set this._model.setSemanticTokens(null, false); } return; } - this._currentDocumentRequestCancellationTokenSource = new CancellationTokenSource(); + + const { provider, request } = r; + this._currentDocumentRequestCancellationTokenSource = cancellationTokenSource; const pendingChanges: IModelContentChangedEvent[] = []; const contentChangeListener = this._model.onDidChangeContent((e) => { @@ -805,15 +808,13 @@ class ModelSemanticColoring extends Disposable { const styling = this._semanticStyling.get(provider); - const lastResultId = this._currentDocumentResponse ? this._currentDocumentResponse.resultId || null : null; - const request = Promise.resolve(provider.provideDocumentSemanticTokens(this._model, lastResultId, this._currentDocumentRequestCancellationTokenSource.token)); - request.then((res) => { this._currentDocumentRequestCancellationTokenSource = null; contentChangeListener.dispose(); this._setDocumentSemanticTokens(provider, res || null, styling, pendingChanges); }, (err) => { - if (!err || typeof err.message !== 'string' || err.message.indexOf('busy') === -1) { + const isExpectedError = err && (errors.isPromiseCanceledError(err) || (typeof err.message === 'string' && err.message.indexOf('busy') !== -1)); + if (!isExpectedError) { errors.onUnexpectedError(err); } @@ -831,14 +832,6 @@ class ModelSemanticColoring extends Disposable { }); } - private static _isSemanticTokens(v: SemanticTokens | SemanticTokensEdits): v is SemanticTokens { - return v && !!((v).data); - } - - private static _isSemanticTokensEdits(v: SemanticTokens | SemanticTokensEdits): v is SemanticTokensEdits { - return v && Array.isArray((v).edits); - } - private static _copy(src: Uint32Array, srcOffset: number, dest: Uint32Array, destOffset: number, length: number): void { for (let i = 0; i < length; i++) { dest[destOffset + i] = src[srcOffset + i]; @@ -847,6 +840,12 @@ class ModelSemanticColoring extends Disposable { private _setDocumentSemanticTokens(provider: DocumentSemanticTokensProvider | null, tokens: SemanticTokens | SemanticTokensEdits | null, styling: SemanticTokensProviderStyling | null, pendingChanges: IModelContentChangedEvent[]): void { const currentResponse = this._currentDocumentResponse; + const rescheduleIfNeeded = () => { + if (pendingChanges.length > 0 && !this._fetchDocumentSemanticTokens.isScheduled()) { + this._fetchDocumentSemanticTokens.schedule(); + } + }; + if (this._currentDocumentResponse) { this._currentDocumentResponse.dispose(); this._currentDocumentResponse = null; @@ -864,10 +863,11 @@ class ModelSemanticColoring extends Disposable { } if (!tokens) { this._model.setSemanticTokens(null, true); + rescheduleIfNeeded(); return; } - if (ModelSemanticColoring._isSemanticTokensEdits(tokens)) { + if (isSemanticTokensEdits(tokens)) { if (!currentResponse) { // not possible! this._model.setSemanticTokens(null, true); @@ -918,7 +918,7 @@ class ModelSemanticColoring extends Disposable { } } - if (ModelSemanticColoring._isSemanticTokens(tokens)) { + if (isSemanticTokens(tokens)) { this._currentDocumentResponse = new SemanticTokensResponse(provider, tokens.resultId, tokens.data); @@ -937,21 +937,13 @@ class ModelSemanticColoring extends Disposable { } } } - - if (!this._fetchDocumentSemanticTokens.isScheduled()) { - this._fetchDocumentSemanticTokens.schedule(); - } } this._model.setSemanticTokens(result, true); - return; + } else { + this._model.setSemanticTokens(null, true); } - this._model.setSemanticTokens(null, true); - } - - private _getSemanticColoringProvider(): DocumentSemanticTokensProvider | null { - const result = DocumentSemanticTokensProviderRegistry.ordered(this._model); - return (result.length > 0 ? result[0] : null); + rescheduleIfNeeded(); } } diff --git a/src/vs/workbench/api/common/shared/semanticTokensDto.ts b/src/vs/editor/common/services/semanticTokensDto.ts similarity index 100% rename from src/vs/workbench/api/common/shared/semanticTokensDto.ts rename to src/vs/editor/common/services/semanticTokensDto.ts diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 8c772e3c..ad85fc86 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -287,11 +287,12 @@ export enum EditorOption { wrappingIndent = 117, wrappingStrategy = 118, showDeprecated = 119, - editorClassName = 120, - pixelRatio = 121, - tabFocusMode = 122, - layoutInfo = 123, - wrappingInfo = 124 + inlineHints = 120, + editorClassName = 121, + pixelRatio = 122, + tabFocusMode = 123, + layoutInfo = 124, + wrappingInfo = 125 } /** diff --git a/src/vs/editor/common/viewLayout/viewLayout.ts b/src/vs/editor/common/viewLayout/viewLayout.ts index 24059d23..9d5583ee 100644 --- a/src/vs/editor/common/viewLayout/viewLayout.ts +++ b/src/vs/editor/common/viewLayout/viewLayout.ts @@ -255,7 +255,7 @@ export class ViewLayout extends Disposable implements IViewLayout { let result = this._linesLayout.getLinesTotalHeight(); if (options.get(EditorOption.scrollBeyondLastLine)) { - result += height - options.get(EditorOption.lineHeight); + result += Math.max(0, height - options.get(EditorOption.lineHeight) - options.get(EditorOption.padding).bottom); } else { result += this._getHorizontalScrollbarHeight(width, contentWidth); } diff --git a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts index 67a3b90f..9640d007 100644 --- a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts +++ b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts @@ -303,6 +303,19 @@ function createLineBreaksFromPreviousLineBreaks(classifier: WrappingCharacterCla breakOffsetVisibleColumn = forcedBreakOffsetVisibleColumn; } + if (breakOffset <= lastBreakingOffset) { + // Make sure that we are advancing (at least one character) + const charCode = lineText.charCodeAt(lastBreakingOffset); + if (strings.isHighSurrogate(charCode)) { + // A surrogate pair must always be considered as a single unit, so it is never to be broken + breakOffset = lastBreakingOffset + 2; + breakOffsetVisibleColumn = lastBreakingOffsetVisibleColumn + 2; + } else { + breakOffset = lastBreakingOffset + 1; + breakOffsetVisibleColumn = lastBreakingOffsetVisibleColumn + computeCharWidth(charCode, lastBreakingOffsetVisibleColumn, tabSize, columnsForFullWidthChar); + } + } + lastBreakingOffset = breakOffset; breakingOffsets[breakingOffsetsCount] = breakOffset; lastBreakingOffsetVisibleColumn = breakOffsetVisibleColumn; @@ -435,6 +448,10 @@ function computeCharWidth(charCode: number, visibleColumn: number, tabSize: numb if (strings.isFullWidthCharacter(charCode)) { return columnsForFullWidthChar; } + if (charCode < 32) { + // when using `editor.renderControlCharacters`, the substitutions are often wide + return columnsForFullWidthChar; + } return 1; } diff --git a/src/vs/editor/common/viewModel/splitLinesCollection.ts b/src/vs/editor/common/viewModel/splitLinesCollection.ts index 50765f8e..89ca9000 100644 --- a/src/vs/editor/common/viewModel/splitLinesCollection.ts +++ b/src/vs/editor/common/viewModel/splitLinesCollection.ts @@ -503,15 +503,8 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return null; } - let hiddenAreas = this.getHiddenAreas(); - let isInHiddenArea = false; - let testPosition = new Position(fromLineNumber, 1); - for (const hiddenArea of hiddenAreas) { - if (hiddenArea.containsPosition(testPosition)) { - isInHiddenArea = true; - break; - } - } + // cannot use this.getHiddenAreas() because those decorations have already seen the effect of this model change + const isInHiddenArea = (fromLineNumber > 2 && !this.lines[fromLineNumber - 2].isVisible()); let outputFromLineNumber = (fromLineNumber === 1 ? 1 : this.prefixSumComputer.getAccumulatedValue(fromLineNumber - 2) + 1); diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 932f6917..a469a79b 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -916,8 +916,8 @@ export class ViewModel extends Disposable implements IViewModel { public getPosition(): Position { return this._cursor.getPrimaryCursorState().modelState.position; } - public setSelections(source: string | null | undefined, selections: readonly ISelection[]): void { - this._withViewEventsCollector(eventsCollector => this._cursor.setSelections(eventsCollector, source, selections)); + public setSelections(source: string | null | undefined, selections: readonly ISelection[], reason = CursorChangeReason.NotSet): void { + this._withViewEventsCollector(eventsCollector => this._cursor.setSelections(eventsCollector, source, selections, reason)); } public saveCursorState(): ICursorState[] { return this._cursor.saveState(); diff --git a/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts b/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts index 0a11a1a5..c59956eb 100644 --- a/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts +++ b/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts @@ -39,20 +39,20 @@ suite('bracket matching', () => { // start on closing bracket editor.setPosition(new Position(1, 20)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 9)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 9)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 19)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 19)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 9)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 9)); // start on opening bracket editor.setPosition(new Position(1, 23)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 31)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 31)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 23)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 23)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 31)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 31)); bracketMatchingController.dispose(); }); @@ -71,25 +71,25 @@ suite('bracket matching', () => { // start position between brackets editor.setPosition(new Position(1, 16)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 18)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 18)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 14)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 14)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 18)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 18)); // skip brackets in comments editor.setPosition(new Position(1, 21)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 23)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 23)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 24)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 24)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 23)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 23)); // do not break if no brackets are available editor.setPosition(new Position(1, 26)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 26)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 26)); bracketMatchingController.dispose(); }); @@ -109,32 +109,32 @@ suite('bracket matching', () => { // start position in open brackets editor.setPosition(new Position(1, 9)); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getPosition(), new Position(1, 20)); - assert.deepEqual(editor.getSelection(), new Selection(1, 9, 1, 20)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 20)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 9, 1, 20)); // start position in close brackets editor.setPosition(new Position(1, 20)); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getPosition(), new Position(1, 20)); - assert.deepEqual(editor.getSelection(), new Selection(1, 9, 1, 20)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 20)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 9, 1, 20)); // start position between brackets editor.setPosition(new Position(1, 16)); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getPosition(), new Position(1, 19)); - assert.deepEqual(editor.getSelection(), new Selection(1, 14, 1, 19)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 19)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 14, 1, 19)); // start position outside brackets editor.setPosition(new Position(1, 21)); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getPosition(), new Position(1, 25)); - assert.deepEqual(editor.getSelection(), new Selection(1, 23, 1, 25)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 25)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 23, 1, 25)); // do not break if no brackets are available editor.setPosition(new Position(1, 26)); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getPosition(), new Position(1, 26)); - assert.deepEqual(editor.getSelection(), new Selection(1, 26, 1, 26)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 26)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 26, 1, 26)); bracketMatchingController.dispose(); }); @@ -159,7 +159,7 @@ suite('bracket matching', () => { editor.setPosition(new Position(3, 5)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getSelection(), new Selection(5, 1, 5, 1)); + assert.deepStrictEqual(editor.getSelection(), new Selection(5, 1, 5, 1)); bracketMatchingController.dispose(); }); @@ -184,7 +184,7 @@ suite('bracket matching', () => { editor.setPosition(new Position(3, 5)); bracketMatchingController.selectToBracket(false); - assert.deepEqual(editor.getSelection(), new Selection(1, 12, 5, 1)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 12, 5, 1)); bracketMatchingController.dispose(); }); @@ -207,7 +207,7 @@ suite('bracket matching', () => { new Selection(1, 17, 1, 17) ]); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(1, 8, 1, 13), new Selection(1, 16, 1, 19) @@ -220,7 +220,7 @@ suite('bracket matching', () => { new Selection(1, 14, 1, 14) ]); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(1, 8, 1, 13), new Selection(1, 16, 1, 19) @@ -233,7 +233,7 @@ suite('bracket matching', () => { new Selection(1, 19, 1, 19) ]); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(1, 8, 1, 13), new Selection(1, 16, 1, 19) diff --git a/src/vs/editor/contrib/clipboard/clipboard.ts b/src/vs/editor/contrib/clipboard/clipboard.ts index 6860a7bb..8a0f08ff 100644 --- a/src/vs/editor/contrib/clipboard/clipboard.ts +++ b/src/vs/editor/contrib/clipboard/clipboard.ts @@ -24,10 +24,11 @@ const CLIPBOARD_CONTEXT_MENU_GROUP = '9_cutcopypaste'; const supportsCut = (platform.isNative || document.queryCommandSupported('cut')); const supportsCopy = (platform.isNative || document.queryCommandSupported('copy')); // IE and Edge have trouble with setting html content in clipboard -const supportsCopyWithSyntaxHighlighting = (supportsCopy && !browser.isEdge); +const supportsCopyWithSyntaxHighlighting = (supportsCopy && !browser.isEdgeLegacy); // Firefox only supports navigator.clipboard.readText() in browser extensions. // See https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText#Browser_compatibility -const supportsPaste = (browser.isFirefox ? document.queryCommandSupported('paste') : true); +// When loading over http, navigator.clipboard can be undefined. See https://github.com/microsoft/monaco-editor/issues/2313 +const supportsPaste = (typeof navigator.clipboard === 'undefined' || browser.isFirefox) ? document.queryCommandSupported('paste') : true; function registerCommand(command: T): T { command.register(); diff --git a/src/vs/editor/contrib/codelens/codelensController.ts b/src/vs/editor/contrib/codelens/codelensController.ts index 3fddfdff..da622e6e 100644 --- a/src/vs/editor/contrib/codelens/codelensController.ts +++ b/src/vs/editor/contrib/codelens/codelensController.ts @@ -98,14 +98,17 @@ export class CodeLensContribution implements IEditorContribution { const fontFamily = this._editor.getOption(EditorOption.codeLensFontFamily); const editorFontInfo = this._editor.getOption(EditorOption.fontInfo); + const fontFamilyVar = `--codelens-font-family${this._styleClassName}`; + let newStyle = ` .monaco-editor .codelens-decoration.${this._styleClassName} { line-height: ${codeLensHeight}px; font-size: ${fontSize}px; padding-right: ${Math.round(fontSize * 0.5)}px; font-feature-settings: ${editorFontInfo.fontFeatureSettings} } .monaco-editor .codelens-decoration.${this._styleClassName} span.codicon { line-height: ${codeLensHeight}px; font-size: ${fontSize}px; } `; if (fontFamily) { - newStyle += `.monaco-editor .codelens-decoration.${this._styleClassName} { font-family: '${fontFamily}'}`; + newStyle += `.monaco-editor .codelens-decoration.${this._styleClassName} { font-family: var(${fontFamilyVar})}`; } this._styleElement.textContent = newStyle; + this._editor.getDomNode()?.style.setProperty(fontFamilyVar, fontFamily ?? 'inherit'); // this._editor.changeViewZones(accessor => { diff --git a/src/vs/editor/contrib/codelens/codelensWidget.ts b/src/vs/editor/contrib/codelens/codelensWidget.ts index f77b2dfc..9014c904 100644 --- a/src/vs/editor/contrib/codelens/codelensWidget.ts +++ b/src/vs/editor/contrib/codelens/codelensWidget.ts @@ -14,7 +14,7 @@ import { editorCodeLensForeground } from 'vs/editor/common/view/editorColorRegis import { CodeLensItem } from 'vs/editor/contrib/codelens/codelens'; import { editorActiveLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { renderCodicons } from 'vs/base/browser/codicons'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; class CodeLensViewZone implements IViewZone { @@ -88,7 +88,7 @@ class CodeLensContentWidget implements IContentWidget { } hasSymbol = true; if (lens.command) { - const title = renderCodicons(lens.command.title.trim()); + const title = renderLabelWithIcons(lens.command.title.trim()); if (lens.command.id) { children.push(dom.$('a', { id: String(i) }, ...title)); this._commands.set(String(i), lens.command); diff --git a/src/vs/editor/contrib/colorPicker/colorContributions.ts b/src/vs/editor/contrib/colorPicker/colorContributions.ts index 4a997061..90f91a84 100644 --- a/src/vs/editor/contrib/colorPicker/colorContributions.ts +++ b/src/vs/editor/contrib/colorPicker/colorContributions.ts @@ -47,7 +47,7 @@ export class ColorContribution extends Disposable implements IEditorContribution } const hoverController = this._editor.getContribution(ModesHoverController.ID); - if (!hoverController.contentWidget.isColorPickerVisible()) { + if (!hoverController.isColorPickerVisible()) { const range = new Range(mouseEvent.target.range.startLineNumber, mouseEvent.target.range.startColumn + 1, mouseEvent.target.range.endLineNumber, mouseEvent.target.range.endColumn + 1); hoverController.showContentHover(range, HoverStartMode.Delayed, false); } diff --git a/src/vs/editor/contrib/comment/test/lineCommentCommand.test.ts b/src/vs/editor/contrib/comment/test/lineCommentCommand.test.ts index 2a4329ba..81b47f07 100644 --- a/src/vs/editor/contrib/comment/test/lineCommentCommand.test.ts +++ b/src/vs/editor/contrib/comment/test/lineCommentCommand.test.ts @@ -96,25 +96,25 @@ suite('Editor Contrib - Line Comment Command', () => { throw new Error(`unexpected`); } - assert.equal(r.shouldRemoveComments, false); + assert.strictEqual(r.shouldRemoveComments, false); // Does not change `commentStr` - assert.equal(r.lines[0].commentStr, '//'); - assert.equal(r.lines[1].commentStr, 'rem'); - assert.equal(r.lines[2].commentStr, '!@#'); - assert.equal(r.lines[3].commentStr, '!@#'); + assert.strictEqual(r.lines[0].commentStr, '//'); + assert.strictEqual(r.lines[1].commentStr, 'rem'); + assert.strictEqual(r.lines[2].commentStr, '!@#'); + assert.strictEqual(r.lines[3].commentStr, '!@#'); // Fills in `isWhitespace` - assert.equal(r.lines[0].ignore, true); - assert.equal(r.lines[1].ignore, true); - assert.equal(r.lines[2].ignore, false); - assert.equal(r.lines[3].ignore, false); + assert.strictEqual(r.lines[0].ignore, true); + assert.strictEqual(r.lines[1].ignore, true); + assert.strictEqual(r.lines[2].ignore, false); + assert.strictEqual(r.lines[3].ignore, false); // Fills in `commentStrOffset` - assert.equal(r.lines[0].commentStrOffset, 2); - assert.equal(r.lines[1].commentStrOffset, 4); - assert.equal(r.lines[2].commentStrOffset, 4); - assert.equal(r.lines[3].commentStrOffset, 2); + assert.strictEqual(r.lines[0].commentStrOffset, 2); + assert.strictEqual(r.lines[1].commentStrOffset, 4); + assert.strictEqual(r.lines[2].commentStrOffset, 4); + assert.strictEqual(r.lines[3].commentStrOffset, 2); r = LineCommentCommand._analyzeLines(Type.Toggle, true, createSimpleModel([ @@ -127,31 +127,31 @@ suite('Editor Contrib - Line Comment Command', () => { throw new Error(`unexpected`); } - assert.equal(r.shouldRemoveComments, true); + assert.strictEqual(r.shouldRemoveComments, true); // Does not change `commentStr` - assert.equal(r.lines[0].commentStr, '//'); - assert.equal(r.lines[1].commentStr, 'rem'); - assert.equal(r.lines[2].commentStr, '!@#'); - assert.equal(r.lines[3].commentStr, '!@#'); + assert.strictEqual(r.lines[0].commentStr, '//'); + assert.strictEqual(r.lines[1].commentStr, 'rem'); + assert.strictEqual(r.lines[2].commentStr, '!@#'); + assert.strictEqual(r.lines[3].commentStr, '!@#'); // Fills in `isWhitespace` - assert.equal(r.lines[0].ignore, true); - assert.equal(r.lines[1].ignore, false); - assert.equal(r.lines[2].ignore, false); - assert.equal(r.lines[3].ignore, false); + assert.strictEqual(r.lines[0].ignore, true); + assert.strictEqual(r.lines[1].ignore, false); + assert.strictEqual(r.lines[2].ignore, false); + assert.strictEqual(r.lines[3].ignore, false); // Fills in `commentStrOffset` - assert.equal(r.lines[0].commentStrOffset, 2); - assert.equal(r.lines[1].commentStrOffset, 4); - assert.equal(r.lines[2].commentStrOffset, 4); - assert.equal(r.lines[3].commentStrOffset, 2); + assert.strictEqual(r.lines[0].commentStrOffset, 2); + assert.strictEqual(r.lines[1].commentStrOffset, 4); + assert.strictEqual(r.lines[2].commentStrOffset, 4); + assert.strictEqual(r.lines[3].commentStrOffset, 2); // Fills in `commentStrLength` - assert.equal(r.lines[0].commentStrLength, 2); - assert.equal(r.lines[1].commentStrLength, 4); - assert.equal(r.lines[2].commentStrLength, 4); - assert.equal(r.lines[3].commentStrLength, 3); + assert.strictEqual(r.lines[0].commentStrLength, 2); + assert.strictEqual(r.lines[1].commentStrLength, 4); + assert.strictEqual(r.lines[2].commentStrLength, 4); + assert.strictEqual(r.lines[3].commentStrLength, 3); }); test('_normalizeInsertionPoint', () => { @@ -166,7 +166,7 @@ suite('Editor Contrib - Line Comment Command', () => { }); LineCommentCommand._normalizeInsertionPoint(model, offsets, 1, tabSize); const actual = offsets.map(item => item.commentStrOffset); - assert.deepEqual(actual, expected, testName); + assert.deepStrictEqual(actual, expected, testName); }; // Bug 16696:[comment] comments not aligned in this case @@ -1083,7 +1083,7 @@ suite('Editor Contrib - Line Comment in mixed modes', () => { tokenize: () => { throw new Error('not implemented'); }, - tokenize2: (line: string, state: modes.IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: modes.IState): TokenizationResult2 => { let languageId = (/^ /.test(line) ? INNER_LANGUAGE_ID : OUTER_LANGUAGE_ID); let tokens = new Uint32Array(1 << 1); diff --git a/src/vs/editor/contrib/cursorUndo/test/cursorUndo.test.ts b/src/vs/editor/contrib/cursorUndo/test/cursorUndo.test.ts index 30848414..4a398b49 100644 --- a/src/vs/editor/contrib/cursorUndo/test/cursorUndo.test.ts +++ b/src/vs/editor/contrib/cursorUndo/test/cursorUndo.test.ts @@ -29,16 +29,16 @@ suite('FindController', () => { // press Delete CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, {}); - assert.deepEqual(editor.getValue(), 'hell'); - assert.deepEqual(editor.getSelections(), [new Selection(1, 5, 1, 5)]); + assert.deepStrictEqual(editor.getValue(), 'hell'); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 5, 1, 5)]); // press left CoreNavigationCommands.CursorLeft.runEditorCommand(null, editor, {}); - assert.deepEqual(editor.getSelections(), [new Selection(1, 4, 1, 4)]); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 4, 1, 4)]); // press Ctrl+U cursorUndoAction.run(null!, editor, {}); - assert.deepEqual(editor.getSelections(), [new Selection(1, 5, 1, 5)]); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 5, 1, 5)]); }); }); @@ -52,12 +52,12 @@ suite('FindController', () => { // type hello editor.trigger('test', Handler.Type, { text: 'hell' }); editor.trigger('test', Handler.Type, { text: 'o' }); - assert.deepEqual(editor.getValue(), 'hello'); - assert.deepEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); + assert.deepStrictEqual(editor.getValue(), 'hello'); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); // press Ctrl+U cursorUndoAction.run(null!, editor, {}); - assert.deepEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); }); }); }); diff --git a/src/vs/editor/contrib/dnd/dnd.ts b/src/vs/editor/contrib/dnd/dnd.ts index b1b4a06b..d535bb90 100644 --- a/src/vs/editor/contrib/dnd/dnd.ts +++ b/src/vs/editor/contrib/dnd/dnd.ts @@ -20,6 +20,7 @@ import { IModelDeltaDecoration } from 'vs/editor/common/model'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; function hasTriggerModifier(e: IKeyboardEvent | IMouseEvent): boolean { if (isMacintosh) { @@ -176,8 +177,8 @@ export class DragAndDropController extends Disposable implements IEditorContribu } }); } - // Use `mouse` as the source instead of `api`. - (this._editor).setSelections(newSelections || [], 'mouse'); + // Use `mouse` as the source instead of `api` and setting the reason to explicit (to behave like any other mouse operation). + (this._editor).setSelections(newSelections || [], 'mouse', CursorChangeReason.Explicit); } else if (!this._dragSelection.containsPosition(newCursorPosition) || ( ( diff --git a/src/vs/editor/contrib/gotoSymbol/documentSymbols.ts b/src/vs/editor/contrib/documentSymbols/documentSymbols.ts similarity index 54% rename from src/vs/editor/contrib/gotoSymbol/documentSymbols.ts rename to src/vs/editor/contrib/documentSymbols/documentSymbols.ts index 51702b59..64f5fa0d 100644 --- a/src/vs/editor/contrib/gotoSymbol/documentSymbols.ts +++ b/src/vs/editor/contrib/documentSymbols/documentSymbols.ts @@ -4,62 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { DocumentSymbol } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { OutlineModel, OutlineElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { OutlineModel } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { assertType } from 'vs/base/common/types'; -import { Iterable } from 'vs/base/common/iterator'; export async function getDocumentSymbols(document: ITextModel, flat: boolean, token: CancellationToken): Promise { - const model = await OutlineModel.create(document, token); - const roots: DocumentSymbol[] = []; - for (const child of model.children.values()) { - if (child instanceof OutlineElement) { - roots.push(child.symbol); - } else { - roots.push(...Iterable.map(child.children.values(), child => child.symbol)); - } - } - - let flatEntries: DocumentSymbol[] = []; - if (token.isCancellationRequested) { - return flatEntries; - } - if (flat) { - flatten(flatEntries, roots, ''); - } else { - flatEntries = roots; - } - - return flatEntries.sort(compareEntriesUsingStart); -} - -function compareEntriesUsingStart(a: DocumentSymbol, b: DocumentSymbol): number { - return Range.compareRangesUsingStarts(a.range, b.range); -} - -function flatten(bucket: DocumentSymbol[], entries: DocumentSymbol[], overrideContainerLabel: string): void { - for (let entry of entries) { - bucket.push({ - kind: entry.kind, - tags: entry.tags, - name: entry.name, - detail: entry.detail, - containerName: entry.containerName || overrideContainerLabel, - range: entry.range, - selectionRange: entry.selectionRange, - children: undefined, // we flatten it... - }); - if (entry.children) { - flatten(bucket, entry.children, entry.name); - } - } + return flat + ? model.asListOfDocumentSymbols() + : model.getTopLevelSymbols(); } CommandsRegistry.registerCommand('_executeDocumentSymbolProvider', async function (accessor, ...args) { diff --git a/src/vs/editor/contrib/documentSymbols/outline.ts b/src/vs/editor/contrib/documentSymbols/outline.ts deleted file mode 100644 index 1dc94901..00000000 --- a/src/vs/editor/contrib/documentSymbols/outline.ts +++ /dev/null @@ -1,18 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; - -export const OutlineViewId = 'outline'; - -export const OutlineViewFiltered = new RawContextKey('outlineFiltered', false); -export const OutlineViewFocused = new RawContextKey('outlineFocused', false); - -export const enum OutlineConfigKeys { - 'icons' = 'outline.icons', - 'problemsEnabled' = 'outline.problems.enabled', - 'problemsColors' = 'outline.problems.colors', - 'problemsBadges' = 'outline.problems.badges' -} diff --git a/src/vs/editor/contrib/documentSymbols/outlineModel.ts b/src/vs/editor/contrib/documentSymbols/outlineModel.ts index c8508150..5638223f 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineModel.ts +++ b/src/vs/editor/contrib/documentSymbols/outlineModel.ts @@ -14,8 +14,8 @@ import { ITextModel } from 'vs/editor/common/model'; import { DocumentSymbol, DocumentSymbolProvider, DocumentSymbolProviderRegistry } from 'vs/editor/common/modes'; import { MarkerSeverity } from 'vs/platform/markers/common/markers'; import { Iterable } from 'vs/base/common/iterator'; -import { URI } from 'vs/base/common/uri'; import { LanguageFeatureRequestDelays } from 'vs/editor/common/modes/languageFeatureRegistry'; +import { URI } from 'vs/base/common/uri'; export abstract class TreeElement { @@ -204,8 +204,6 @@ export class OutlineGroup extends TreeElement { } } - - export class OutlineModel extends TreeElement { private static readonly _requestDurations = new LanguageFeatureRequestDelays(DocumentSymbolProviderRegistry, 350); @@ -445,4 +443,43 @@ export class OutlineModel extends TreeElement { group.updateMarker(marker.slice(0)); } } + + getTopLevelSymbols(): DocumentSymbol[] { + const roots: DocumentSymbol[] = []; + for (const child of this.children.values()) { + if (child instanceof OutlineElement) { + roots.push(child.symbol); + } else { + roots.push(...Iterable.map(child.children.values(), child => child.symbol)); + } + } + return roots.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + } + + asListOfDocumentSymbols(): DocumentSymbol[] { + const roots = this.getTopLevelSymbols(); + const bucket: DocumentSymbol[] = []; + OutlineModel._flattenDocumentSymbols(bucket, roots, ''); + return bucket.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + } + + private static _flattenDocumentSymbols(bucket: DocumentSymbol[], entries: DocumentSymbol[], overrideContainerLabel: string): void { + for (const entry of entries) { + bucket.push({ + kind: entry.kind, + tags: entry.tags, + name: entry.name, + detail: entry.detail, + containerName: entry.containerName || overrideContainerLabel, + range: entry.range, + selectionRange: entry.selectionRange, + children: undefined, // we flatten it... + }); + + // Recurse over children + if (entry.children) { + OutlineModel._flattenDocumentSymbols(bucket, entry.children, entry.name); + } + } + } } diff --git a/src/vs/editor/contrib/find/findWidget.css b/src/vs/editor/contrib/find/findWidget.css index 15ed9eb3..86efb77c 100644 --- a/src/vs/editor/contrib/find/findWidget.css +++ b/src/vs/editor/contrib/find/findWidget.css @@ -60,14 +60,14 @@ } -.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .mirror { +.monaco-editor .find-widget > .replace-part .monaco-inputbox > .ibwrapper > .mirror { padding-right: 22px; } -.monaco-editor .find-widget > .find-part .monaco-inputbox > .wrapper > .input, -.monaco-editor .find-widget > .find-part .monaco-inputbox > .wrapper > .mirror, -.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .input, -.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .mirror { +.monaco-editor .find-widget > .find-part .monaco-inputbox > .ibwrapper > .input, +.monaco-editor .find-widget > .find-part .monaco-inputbox > .ibwrapper > .mirror, +.monaco-editor .find-widget > .replace-part .monaco-inputbox > .ibwrapper > .input, +.monaco-editor .find-widget > .replace-part .monaco-inputbox > .ibwrapper > .mirror { padding-top: 2px; padding-bottom: 2px; } diff --git a/src/vs/editor/contrib/find/findWidget.ts b/src/vs/editor/contrib/find/findWidget.ts index 597af6c3..89981ba4 100644 --- a/src/vs/editor/contrib/find/findWidget.ts +++ b/src/vs/editor/contrib/find/findWidget.ts @@ -1044,7 +1044,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL // Toggle selection button this._toggleSelectionFind = this._register(new Checkbox({ - icon: ThemeIcon.asCSSIcon(findSelectionIcon), + icon: findSelectionIcon, title: NLS_TOGGLE_SELECTION_FIND_TITLE + this._keybindingLabelFor(FIND_IDS.ToggleSearchScopeCommand), isChecked: false })); diff --git a/src/vs/editor/contrib/find/test/find.test.ts b/src/vs/editor/contrib/find/test/find.test.ts index 98f31f32..55490860 100644 --- a/src/vs/editor/contrib/find/test/find.test.ts +++ b/src/vs/editor/contrib/find/test/find.test.ts @@ -20,17 +20,17 @@ suite('Find', () => { // The cursor is at the very top, of the file, at the first ABC let searchStringAtTop = getSelectionSearchString(editor); - assert.equal(searchStringAtTop, 'ABC'); + assert.strictEqual(searchStringAtTop, 'ABC'); // Move cursor to the end of ABC editor.setPosition(new Position(1, 3)); let searchStringAfterABC = getSelectionSearchString(editor); - assert.equal(searchStringAfterABC, 'ABC'); + assert.strictEqual(searchStringAfterABC, 'ABC'); // Move cursor to DEF editor.setPosition(new Position(1, 5)); let searchStringInsideDEF = getSelectionSearchString(editor); - assert.equal(searchStringInsideDEF, 'DEF'); + assert.strictEqual(searchStringInsideDEF, 'DEF'); }); }); @@ -44,17 +44,17 @@ suite('Find', () => { // Select A of ABC editor.setSelection(new Range(1, 1, 1, 2)); let searchStringSelectionA = getSelectionSearchString(editor); - assert.equal(searchStringSelectionA, 'A'); + assert.strictEqual(searchStringSelectionA, 'A'); // Select BC of ABC editor.setSelection(new Range(1, 2, 1, 4)); let searchStringSelectionBC = getSelectionSearchString(editor); - assert.equal(searchStringSelectionBC, 'BC'); + assert.strictEqual(searchStringSelectionBC, 'BC'); // Select BC DE editor.setSelection(new Range(1, 2, 1, 7)); let searchStringSelectionBCDE = getSelectionSearchString(editor); - assert.equal(searchStringSelectionBCDE, 'BC DE'); + assert.strictEqual(searchStringSelectionBCDE, 'BC DE'); }); }); @@ -68,17 +68,17 @@ suite('Find', () => { // Select first line and newline editor.setSelection(new Range(1, 1, 2, 1)); let searchStringSelectionWholeLine = getSelectionSearchString(editor); - assert.equal(searchStringSelectionWholeLine, null); + assert.strictEqual(searchStringSelectionWholeLine, null); // Select first line and chunk of second editor.setSelection(new Range(1, 1, 2, 4)); let searchStringSelectionTwoLines = getSelectionSearchString(editor); - assert.equal(searchStringSelectionTwoLines, null); + assert.strictEqual(searchStringSelectionTwoLines, null); // Select end of first line newline and chunk of second editor.setSelection(new Range(1, 7, 2, 4)); let searchStringSelectionSpanLines = getSelectionSearchString(editor); - assert.equal(searchStringSelectionSpanLines, null); + assert.strictEqual(searchStringSelectionSpanLines, null); }); }); diff --git a/src/vs/editor/contrib/find/test/findController.test.ts b/src/vs/editor/contrib/find/test/findController.test.ts index 8d9216ea..3f55711c 100644 --- a/src/vs/editor/contrib/find/test/findController.test.ts +++ b/src/vs/editor/contrib/find/test/findController.test.ts @@ -102,7 +102,7 @@ suite('FindController', async () => { // I hit Ctrl+F to show the Find dialog startFindAction.run(null, editor); - assert.deepEqual(findController.getGlobalBufferTerm(), findController.getState().searchString); + assert.deepStrictEqual(findController.getGlobalBufferTerm(), findController.getState().searchString); findController.dispose(); }); }); @@ -126,9 +126,9 @@ suite('FindController', async () => { let nextMatchFindAction = new NextMatchFindAction(); nextMatchFindAction.run(null, editor); - assert.equal(findState.searchString, 'ABC'); + assert.strictEqual(findState.searchString, 'ABC'); - assert.deepEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]); findController.dispose(); }); @@ -152,7 +152,7 @@ suite('FindController', async () => { findState.change({ searchString: 'ABC' }, true); - assert.deepEqual(findController.getGlobalBufferTerm(), 'ABC'); + assert.deepStrictEqual(findController.getGlobalBufferTerm(), 'ABC'); findController.dispose(); }); @@ -181,14 +181,14 @@ suite('FindController', async () => { findState.change({ searchString: 'ABC' }, true); // The first ABC is highlighted. - assert.deepEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]); // I hit Esc to exit the Find dialog. findController.closeFindWidget(); findController.hasFocus = false; // The cursor is now at end of the first line, with ABC on that line highlighted. - assert.deepEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]); // I hit delete to remove it and change the text to XYZ. editor.pushUndoStop(); @@ -201,16 +201,16 @@ suite('FindController', async () => { // ABC // XYZ // ABC - assert.equal(editor.getModel()!.getLineContent(1), 'XYZ'); + assert.strictEqual(editor.getModel()!.getLineContent(1), 'XYZ'); // The cursor is at end of the first line. - assert.deepEqual(fromSelection(editor.getSelection()!), [1, 4, 1, 4]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 4, 1, 4]); // I hit F3 to "Find Next" to find the next occurrence of ABC, but instead it searches for XYZ. await nextMatchFindAction.run(null, editor); - assert.equal(findState.searchString, 'ABC'); - assert.equal(findController.hasFocus, false); + assert.strictEqual(findState.searchString, 'ABC'); + assert.strictEqual(findController.hasFocus, false); findController.dispose(); }); @@ -230,10 +230,10 @@ suite('FindController', async () => { }); await nextMatchFindAction.run(null, editor); - assert.deepEqual(fromSelection(editor.getSelection()!), [1, 26, 1, 29]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 26, 1, 29]); await nextMatchFindAction.run(null, editor); - assert.deepEqual(fromSelection(editor.getSelection()!), [1, 8, 1, 11]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 8, 1, 11]); findController.dispose(); }); @@ -256,10 +256,10 @@ suite('FindController', async () => { await startFindAction.run(null, editor); await nextMatchFindAction.run(null, editor); - assert.deepEqual(fromSelection(editor.getSelection()!), [2, 9, 2, 13]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [2, 9, 2, 13]); await nextMatchFindAction.run(null, editor); - assert.deepEqual(fromSelection(editor.getSelection()!), [1, 9, 1, 13]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 9, 1, 13]); findController.dispose(); }); @@ -288,7 +288,7 @@ suite('FindController', async () => { await nextMatchFindAction.run(null, editor); await startFindReplaceAction.run(null, editor); - assert.equal(findController.getState().searchString, testRegexString); + assert.strictEqual(findController.getState().searchString, testRegexString); findController.dispose(); }); @@ -312,16 +312,16 @@ suite('FindController', async () => { loop: true }); - assert.equal(findController.getState().searchScope, null); + assert.strictEqual(findController.getState().searchScope, null); findController.getState().change({ searchScope: [new Range(1, 1, 1, 5)] }, false); - assert.deepEqual(findController.getState().searchScope, [new Range(1, 1, 1, 5)]); + assert.deepStrictEqual(findController.getState().searchScope, [new Range(1, 1, 1, 5)]); findController.closeFindWidget(); - assert.equal(findController.getState().searchScope, null); + assert.strictEqual(findController.getState().searchScope, null); }); }); @@ -338,13 +338,13 @@ suite('FindController', async () => { findController.getState().change({ searchString: '\\b\\s{3}\\b', replaceString: ' ', isRegex: true }, false); findController.moveToNextMatch(); - assert.deepEqual(editor.getSelections()!.map(fromSelection), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromSelection), [ [1, 39, 1, 42] ]); findController.replace(); - assert.deepEqual(editor.getValue(), 'HRESULT OnAmbientPropertyChange(DISPID dispid);'); + assert.deepStrictEqual(editor.getValue(), 'HRESULT OnAmbientPropertyChange(DISPID dispid);'); findController.dispose(); }); @@ -365,13 +365,13 @@ suite('FindController', async () => { findController.getState().change({ searchString: '^', replaceString: 'x', isRegex: true }, false); findController.moveToNextMatch(); - assert.deepEqual(editor.getSelections()!.map(fromSelection), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromSelection), [ [2, 1, 2, 1] ]); findController.replace(); - assert.deepEqual(editor.getValue(), '\nxline2\nline3'); + assert.deepStrictEqual(editor.getValue(), '\nxline2\nline3'); findController.dispose(); }); @@ -396,7 +396,7 @@ suite('FindController', async () => { // cmd+f3 await nextSelectionMatchFindAction.run(null, editor); - assert.deepEqual(editor.getSelections()!.map(fromSelection), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromSelection), [ [3, 1, 3, 9] ]); @@ -427,7 +427,7 @@ suite('FindController', async () => { // cmd+f3 await nextSelectionMatchFindAction.run(null, editor); - assert.deepEqual(editor.getSelections()!.map(fromSelection), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromSelection), [ [3, 1, 3, 9] ]); @@ -458,7 +458,7 @@ suite('FindController', async () => { await startFindWithSelectionAction.run(null, editor); let findState = findController.getState(); - assert.deepEqual(findState.searchString.split(/\r\n|\r|\n/g), ['ABC', 'ABC']); + assert.deepStrictEqual(findState.searchString.split(/\r\n|\r|\n/g), ['ABC', 'ABC']); editor.setSelection(new Selection(3, 1, 3, 1)); await startFindWithSelectionAction.run(null, editor); @@ -483,7 +483,7 @@ suite('FindController', async () => { startFindWithSelectionAction.run(null, editor); let findState = findController.getState(); - assert.deepEqual(findState.searchString, 'ABC'); + assert.deepStrictEqual(findState.searchString, 'ABC'); findController.dispose(); }); }); @@ -531,7 +531,7 @@ suite('FindController query options persistence', async () => { // I type ABC. findState.change({ searchString: 'ABC' }, true); // The second ABC is highlighted as matchCase is true. - assert.deepEqual(fromSelection(editor.getSelection()!), [2, 1, 2, 4]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [2, 1, 2, 4]); findController.dispose(); }); @@ -558,7 +558,7 @@ suite('FindController query options persistence', async () => { // I type AB. findState.change({ searchString: 'AB' }, true); // The second AB is highlighted as wholeWord is true. - assert.deepEqual(fromSelection(editor.getSelection()!), [2, 1, 2, 3]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [2, 1, 2, 3]); findController.dispose(); }); @@ -575,7 +575,7 @@ suite('FindController query options persistence', async () => { // The cursor is at the very top, of the file, at the first ABC let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); findController.toggleRegex(); - assert.equal(queryState['editor.isRegex'], true); + assert.strictEqual(queryState['editor.isRegex'], true); findController.dispose(); }); @@ -601,13 +601,13 @@ suite('FindController query options persistence', async () => { editor.setSelection(new Range(1, 1, 2, 1)); findController.start(findConfig); - assert.deepEqual(findController.getState().searchScope, [new Selection(1, 1, 2, 1)]); + assert.deepStrictEqual(findController.getState().searchScope, [new Selection(1, 1, 2, 1)]); findController.closeFindWidget(); editor.setSelections([new Selection(1, 1, 2, 1), new Selection(2, 1, 2, 5)]); findController.start(findConfig); - assert.deepEqual(findController.getState().searchScope, [new Selection(1, 1, 2, 1), new Selection(2, 1, 2, 5)]); + assert.deepStrictEqual(findController.getState().searchScope, [new Selection(1, 1, 2, 1), new Selection(2, 1, 2, 5)]); }); }); @@ -631,7 +631,7 @@ suite('FindController query options persistence', async () => { loop: true }); - assert.deepEqual(findController.getState().searchScope, null); + assert.deepStrictEqual(findController.getState().searchScope, null); }); }); @@ -655,7 +655,7 @@ suite('FindController query options persistence', async () => { loop: true }); - assert.deepEqual(findController.getState().searchScope, [new Selection(1, 2, 1, 3)]); + assert.deepStrictEqual(findController.getState().searchScope, [new Selection(1, 2, 1, 3)]); }); }); @@ -680,7 +680,7 @@ suite('FindController query options persistence', async () => { loop: true }); - assert.deepEqual(findController.getState().searchScope, [new Selection(1, 6, 2, 1)]); + assert.deepStrictEqual(findController.getState().searchScope, [new Selection(1, 6, 2, 1)]); }); }); }); diff --git a/src/vs/editor/contrib/find/test/findModel.test.ts b/src/vs/editor/contrib/find/test/findModel.test.ts index 70c54949..10c7ae32 100644 --- a/src/vs/editor/contrib/find/test/findModel.test.ts +++ b/src/vs/editor/contrib/find/test/findModel.test.ts @@ -81,13 +81,13 @@ suite('FindModel', () => { } function assertFindState(editor: ICodeEditor, cursor: number[], highlighted: number[] | null, findDecorations: number[][]): void { - assert.deepEqual(fromRange(editor.getSelection()!), cursor, 'cursor'); + assert.deepStrictEqual(fromRange(editor.getSelection()!), cursor, 'cursor'); let expectedState = { highlighted: highlighted ? [highlighted] : [], findDecorations: findDecorations }; - assert.deepEqual(_getFindState(editor), expectedState, 'state'); + assert.deepStrictEqual(_getFindState(editor), expectedState, 'state'); } findTest('incremental find from beginning of file', (editor) => { @@ -245,7 +245,7 @@ suite('FindModel', () => { findState.change({ searchString: 'hello' }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); - assert.equal(findState.matchesCount, 5); + assert.strictEqual(findState.matchesCount, 5); assertFindState( editor, [1, 1, 1, 1], @@ -275,7 +275,7 @@ suite('FindModel', () => { findState.change({ searchString: 'hello' }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); - assert.equal(findState.matchesCount, 5); + assert.strictEqual(findState.matchesCount, 5); assertFindState( editor, [1, 1, 1, 1], @@ -290,7 +290,7 @@ suite('FindModel', () => { ); findState.change({ searchString: 'helloo' }, false); - assert.equal(findState.matchesCount, 0); + assert.strictEqual(findState.matchesCount, 0); assertFindState( editor, [1, 1, 1, 1], @@ -1306,7 +1306,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); findModel.replace(); assertFindState( @@ -1320,7 +1320,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); findModel.replace(); assertFindState( @@ -1333,7 +1333,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello world, hi!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello world, hi!" << endl;'); findModel.replace(); assertFindState( @@ -1345,7 +1345,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); findModel.replace(); assertFindState( @@ -1356,7 +1356,7 @@ suite('FindModel', () => { [6, 14, 6, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); findModel.replace(); assertFindState( @@ -1365,7 +1365,7 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hi world, hi!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hi world, hi!" << endl;'); findModel.dispose(); findState.dispose(); @@ -1398,7 +1398,7 @@ suite('FindModel', () => { [11, 10, 11, 13] ] ); - assert.equal(editor.getModel()!.getLineContent(11), '// blablablaciao'); + assert.strictEqual(editor.getModel()!.getLineContent(11), '// blablablaciao'); findModel.replace(); assertFindState( @@ -1410,7 +1410,7 @@ suite('FindModel', () => { [11, 11, 11, 14] ] ); - assert.equal(editor.getModel()!.getLineContent(11), '// ciaoblablaciao'); + assert.strictEqual(editor.getModel()!.getLineContent(11), '// ciaoblablaciao'); findModel.replace(); assertFindState( @@ -1421,7 +1421,7 @@ suite('FindModel', () => { [11, 12, 11, 15] ] ); - assert.equal(editor.getModel()!.getLineContent(11), '// ciaociaoblaciao'); + assert.strictEqual(editor.getModel()!.getLineContent(11), '// ciaociaoblaciao'); findModel.replace(); assertFindState( @@ -1430,7 +1430,7 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(11), '// ciaociaociaociao'); + assert.strictEqual(editor.getModel()!.getLineContent(11), '// ciaociaociaociao'); findModel.dispose(); findState.dispose(); @@ -1467,7 +1467,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); findModel.replaceAll(); assertFindState( @@ -1476,9 +1476,9 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hi world, hi!" << endl;'); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hi world, hi!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); findModel.dispose(); findState.dispose(); @@ -1517,10 +1517,10 @@ suite('FindModel', () => { [9, 1, 9, 3] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hello world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "Hello world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(9), ' cout << "helloworld again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hello world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "Hello world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(9), ' cout << "helloworld again" << endl;'); findModel.dispose(); findState.dispose(); @@ -1549,7 +1549,7 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(11), '// ciaociaociaociao'); + assert.strictEqual(editor.getModel()!.getLineContent(11), '// ciaociaociaociao'); findModel.dispose(); findState.dispose(); @@ -1578,10 +1578,10 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(11), '// <'); - assert.equal(editor.getModel()!.getLineContent(12), '\t><'); - assert.equal(editor.getModel()!.getLineContent(13), '\t><'); - assert.equal(editor.getModel()!.getLineContent(14), '\t>ciao'); + assert.strictEqual(editor.getModel()!.getLineContent(11), '// <'); + assert.strictEqual(editor.getModel()!.getLineContent(12), '\t><'); + assert.strictEqual(editor.getModel()!.getLineContent(13), '\t><'); + assert.strictEqual(editor.getModel()!.getLineContent(14), '\t>ciao'); findModel.dispose(); findState.dispose(); @@ -1610,8 +1610,8 @@ suite('FindModel', () => { [] ); - assert.equal(editor.getModel()!.getLineContent(2), '#bar "cool.h"'); - assert.equal(editor.getModel()!.getLineContent(3), '#bar '); + assert.strictEqual(editor.getModel()!.getLineContent(2), '#bar "cool.h"'); + assert.strictEqual(editor.getModel()!.getLineContent(3), '#bar '); findModel.dispose(); findState.dispose(); @@ -1665,7 +1665,7 @@ suite('FindModel', () => { findModel.selectAllMatches(); - assert.deepEqual(editor!.getSelections()!.map(s => s.toString()), [ + assert.deepStrictEqual(editor!.getSelections()!.map(s => s.toString()), [ new Selection(6, 14, 6, 19), new Selection(6, 27, 6, 32), new Selection(7, 14, 7, 19), @@ -1709,14 +1709,14 @@ suite('FindModel', () => { findModel.selectAllMatches(); - assert.deepEqual(editor!.getSelections()!.map(s => s.toString()), [ + assert.deepStrictEqual(editor!.getSelections()!.map(s => s.toString()), [ new Selection(7, 14, 7, 19), new Selection(6, 14, 6, 19), new Selection(6, 27, 6, 32), new Selection(8, 14, 8, 19) ].map(s => s.toString())); - assert.deepEqual(editor!.getSelection()!.toString(), new Selection(7, 14, 7, 19).toString()); + assert.deepStrictEqual(editor!.getSelection()!.toString(), new Selection(7, 14, 7, 19).toString()); assertFindState( editor, @@ -1800,7 +1800,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); findModel.replace(); assertFindState( @@ -1812,7 +1812,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); findModel.replace(); assertFindState( @@ -1823,7 +1823,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); findModel.replace(); assertFindState( @@ -1832,7 +1832,7 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); findModel.dispose(); findState.dispose(); @@ -1871,7 +1871,7 @@ suite('FindModel', () => { ] ); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "Hello world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "Hello world again" << endl;'); findModel.replace(); assertFindState( @@ -1883,7 +1883,7 @@ suite('FindModel', () => { [7, 14, 7, 19], ] ); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); findModel.replace(); assertFindState( @@ -1894,7 +1894,7 @@ suite('FindModel', () => { [7, 14, 7, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); findModel.replace(); assertFindState( @@ -1903,7 +1903,7 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); findModel.dispose(); findState.dispose(); @@ -1927,9 +1927,9 @@ suite('FindModel', () => { findModel.replaceAll(); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); assertFindState( editor, @@ -1970,7 +1970,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); findModel.replace(); assertFindState( @@ -1982,7 +1982,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hilo world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hilo world, Hello!" << endl;'); findModel.replace(); assertFindState( @@ -1993,7 +1993,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hilo world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hilo world again" << endl;'); findModel.replace(); assertFindState( @@ -2002,7 +2002,7 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "hilo world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "hilo world again" << endl;'); findModel.dispose(); findState.dispose(); @@ -2027,10 +2027,10 @@ suite('FindModel', () => { findModel.replaceAll(); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello girl, Hello!" << endl;'); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hello girl again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "Hello girl again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(9), ' cout << "hellogirl again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello girl, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hello girl again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "Hello girl again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(9), ' cout << "hellogirl again" << endl;'); assertFindState( editor, @@ -2060,8 +2060,8 @@ suite('FindModel', () => { findModel.replaceAll(); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello girl, Hello!" << endl;'); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "Hello girl again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello girl, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "Hello girl again" << endl;'); assertFindState( editor, @@ -2094,10 +2094,10 @@ suite('FindModel', () => { findModel.replaceAll(); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "goodbye world, Goodbye!" << endl;'); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "goodbye world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "Goodbye world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(9), ' cout << "goodbyeworld again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "goodbye world, Goodbye!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "goodbye world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "Goodbye world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(9), ' cout << "goodbyeworld again" << endl;'); assertFindState( editor, @@ -2134,9 +2134,9 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << " world, !" << endl;'); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << " world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << " world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << " world, !" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << " world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << " world again" << endl;'); findModel.dispose(); findState.dispose(); @@ -2159,7 +2159,7 @@ suite('FindModel', () => { expectedText += 'a line' + i + '\n'; } expectedText += 'a '; - assert.equal(editor!.getModel()!.getValue(), expectedText); + assert.strictEqual(editor!.getModel()!.getValue(), expectedText); findModel.dispose(); findState.dispose(); @@ -2188,9 +2188,9 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(9), ' cout << "hiworld again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(9), ' cout << "hiworld again" << endl;'); findModel.dispose(); findState.dispose(); @@ -2219,78 +2219,78 @@ suite('FindModel', () => { findState.change({ searchString: 'hello', loop: false }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); - assert.equal(findState.matchesCount, 5); + assert.strictEqual(findState.matchesCount, 5); // Test next operations - assert.equal(findState.matchesPosition, 0); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 0); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), false); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), false); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 2); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 2); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 3); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 3); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 4); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 4); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 5); - assert.equal(findState.canNavigateForward(), false); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 5); + assert.strictEqual(findState.canNavigateForward(), false); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 5); - assert.equal(findState.canNavigateForward(), false); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 5); + assert.strictEqual(findState.canNavigateForward(), false); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 5); - assert.equal(findState.canNavigateForward(), false); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 5); + assert.strictEqual(findState.canNavigateForward(), false); + assert.strictEqual(findState.canNavigateBack(), true); // Test previous operations findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 4); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 4); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 3); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 3); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 2); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 2); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), false); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), false); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), false); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), false); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), false); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), false); }); @@ -2299,78 +2299,78 @@ suite('FindModel', () => { findState.change({ searchString: 'hello' }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); - assert.equal(findState.matchesCount, 5); + assert.strictEqual(findState.matchesCount, 5); // Test next operations - assert.equal(findState.matchesPosition, 0); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 0); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 2); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 2); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 3); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 3); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 4); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 4); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 5); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 5); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 2); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 2); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); // Test previous operations findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 5); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 5); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 4); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 4); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 3); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 3); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 2); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 2); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); }); diff --git a/src/vs/editor/contrib/find/test/replacePattern.test.ts b/src/vs/editor/contrib/find/test/replacePattern.test.ts index 907292fd..d9491864 100644 --- a/src/vs/editor/contrib/find/test/replacePattern.test.ts +++ b/src/vs/editor/contrib/find/test/replacePattern.test.ts @@ -13,7 +13,7 @@ suite('Replace Pattern test', () => { let testParse = (input: string, expectedPieces: ReplacePiece[]) => { let actual = parseReplaceString(input); let expected = new ReplacePattern(expectedPieces); - assert.deepEqual(actual, expected, 'Parsing ' + input); + assert.deepStrictEqual(actual, expected, 'Parsing ' + input); }; // no backslash => no treatment @@ -73,14 +73,14 @@ suite('Replace Pattern test', () => { let testParse = (input: string, expectedPieces: ReplacePiece[]) => { let actual = parseReplaceString(input); let expected = new ReplacePattern(expectedPieces); - assert.deepEqual(actual, expected, 'Parsing ' + input); + assert.deepStrictEqual(actual, expected, 'Parsing ' + input); }; function assertReplace(target: string, search: RegExp, replaceString: string, expected: string): void { let replacePattern = parseReplaceString(replaceString); let m = search.exec(target); let actual = replacePattern.buildReplaceString(m); - assert.equal(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`); + assert.strictEqual(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`); } // \U, \u => uppercase \L, \l => lowercase \E => cancel @@ -107,7 +107,7 @@ suite('Replace Pattern test', () => { let m = search.exec(target); let actual = replacePattern.buildReplaceString(m); - assert.deepEqual(actual, expected, `${target}.replace(${search}, ${replaceString})`); + assert.deepStrictEqual(actual, expected, `${target}.replace(${search}, ${replaceString})`); }; testJSReplaceSemantics('hi', /hi/, 'hello', 'hi'.replace(/hi/, 'hello')); @@ -136,7 +136,7 @@ suite('Replace Pattern test', () => { let m = search.exec(target); let actual = replacePattern.buildReplaceString(m); - assert.equal(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`); + assert.strictEqual(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`); } assertReplace('bla', /bla/, 'hello', 'hello'); @@ -162,7 +162,7 @@ suite('Replace Pattern test', () => { let m = search.exec(target); let actual = replacePattern.buildReplaceString(m); - assert.equal(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`); + assert.strictEqual(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`); } assertReplace('this is a bla text', /bla/, 'hello', 'hello'); assertReplace('this is a bla text', /this(?=.*bla)/, 'that', 'that'); @@ -184,14 +184,14 @@ suite('Replace Pattern test', () => { let replacePattern = parseReplaceString('a{$1}'); let matches = /a(z)?/.exec('abcd'); let actual = replacePattern.buildReplaceString(matches); - assert.equal(actual, 'a{}'); + assert.strictEqual(actual, 'a{}'); }); test('buildReplaceStringWithCasePreserved test', () => { function assertReplace(target: string[], replaceString: string, expected: string): void { let actual: string = ''; actual = buildReplaceStringWithCasePreserved(target, replaceString); - assert.equal(actual, expected); + assert.strictEqual(actual, expected); } assertReplace(['abc'], 'Def', 'def'); @@ -219,7 +219,7 @@ suite('Replace Pattern test', () => { function assertReplace(target: string[], replaceString: string, expected: string): void { let replacePattern = parseReplaceString(replaceString); let actual = replacePattern.buildReplaceString(target, true); - assert.equal(actual, expected); + assert.strictEqual(actual, expected); } assertReplace(['abc'], 'Def', 'def'); diff --git a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts index 10a503f6..8096baa1 100644 --- a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts @@ -16,13 +16,12 @@ import { Color } from 'vs/base/common/color'; import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { ScrollType } from 'vs/editor/common/editorCommon'; -import { getBaseLabel, getPathLabel } from 'vs/base/common/labels'; +import { getBaseLabel } from 'vs/base/common/labels'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { Event, Emitter } from 'vs/base/common/event'; import { PeekViewWidget, peekViewTitleForeground, peekViewTitleInfoForeground } from 'vs/editor/contrib/peekView/peekView'; import { basename } from 'vs/base/common/resources'; import { IAction } from 'vs/base/common/actions'; -import { IActionBarOptions, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -31,6 +30,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { splitLines } from 'vs/base/common/strings'; +import { ILabelService } from 'vs/platform/label/common/label'; class MessageWidget { @@ -51,6 +51,7 @@ class MessageWidget { editor: ICodeEditor, onRelatedInformation: (related: IRelatedInformation) => void, private readonly _openerService: IOpenerService, + private readonly _labelService: ILabelService ) { this._editor = editor; @@ -169,7 +170,7 @@ class MessageWidget { let relatedResource = document.createElement('a'); relatedResource.classList.add('filename'); relatedResource.innerText = `${getBaseLabel(related.resource)}(${related.startLineNumber}, ${related.startColumn}): `; - relatedResource.title = getPathLabel(related.resource, undefined); + relatedResource.title = this._labelService.getUriLabel(related.resource); this._relatedDiagnostics.set(relatedResource, related); let relatedMessage = document.createElement('span'); @@ -248,7 +249,8 @@ export class MarkerNavigationWidget extends PeekViewWidget { @IOpenerService private readonly _openerService: IOpenerService, @IMenuService private readonly _menuService: IMenuService, @IInstantiationService instantiationService: IInstantiationService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @ILabelService private readonly _labelService: ILabelService ) { super(editor, { showArrow: true, showFrame: true, isAccessible: true }, instantiationService); this._severity = MarkerSeverity.Warning; @@ -310,13 +312,6 @@ export class MarkerNavigationWidget extends PeekViewWidget { this._icon = dom.append(container, dom.$('')); } - protected _getActionBarOptions(): IActionBarOptions { - return { - ...super._getActionBarOptions(), - orientation: ActionsOrientation.HORIZONTAL - }; - } - protected _fillBody(container: HTMLElement): void { this._parentContainer = container; container.classList.add('marker-widget'); @@ -326,7 +321,7 @@ export class MarkerNavigationWidget extends PeekViewWidget { this._container = document.createElement('div'); container.appendChild(this._container); - this._message = new MessageWidget(this._container, this.editor, related => this._onDidSelectRelatedInformation.fire(related), this._openerService); + this._message = new MessageWidget(this._container, this.editor, related => this._onDidSelectRelatedInformation.fire(related), this._openerService, this._labelService); this._disposables.add(this._message); } diff --git a/src/vs/editor/contrib/gotoSymbol/referencesModel.ts b/src/vs/editor/contrib/gotoSymbol/referencesModel.ts index 334928f3..63f6b952 100644 --- a/src/vs/editor/contrib/gotoSymbol/referencesModel.ts +++ b/src/vs/editor/contrib/gotoSymbol/referencesModel.ts @@ -51,7 +51,7 @@ export class OneReference { ); } else { return localize( - 'aria.oneReference.preview', "symbol in {0} on line {1} at column {2}, {3}", + { key: 'aria.oneReference.preview', comment: ['Placeholders are: 0: filename, 1:line number, 2: column number, 3: preview snippet of source code'] }, "symbol in {0} on line {1} at column {2}, {3}", basename(this.uri), this.range.startLineNumber, this.range.startColumn, preview.value ); } diff --git a/src/vs/editor/contrib/hover/hover.ts b/src/vs/editor/contrib/hover/hover.ts index c57fbadb..071253cb 100644 --- a/src/vs/editor/contrib/hover/hover.ts +++ b/src/vs/editor/contrib/hover/hover.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { IDisposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IEmptyContentData } from 'vs/editor/browser/controller/mouseTarget'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; @@ -22,11 +22,10 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { IOpenerService } from 'vs/platform/opener/common/opener'; import { editorHoverBackground, editorHoverBorder, editorHoverHighlight, textCodeBlockBackground, textLinkForeground, editorHoverStatusBarBackground, editorHoverForeground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDecorationService'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; import { GotoDefinitionAtPositionEditorContribution } from 'vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export class ModesHoverController implements IEditorContribution { @@ -35,22 +34,8 @@ export class ModesHoverController implements IEditorContribution { private readonly _toUnhook = new DisposableStore(); private readonly _didChangeConfigurationHandler: IDisposable; - private readonly _contentWidget = new MutableDisposable(); - private readonly _glyphWidget = new MutableDisposable(); - - get contentWidget(): ModesContentHoverWidget { - if (!this._contentWidget.value) { - this._createHoverWidgets(); - } - return this._contentWidget.value!; - } - - get glyphWidget(): ModesGlyphHoverWidget { - if (!this._glyphWidget.value) { - this._createHoverWidgets(); - } - return this._glyphWidget.value!; - } + private _contentWidget: ModesContentHoverWidget | null; + private _glyphWidget: ModesGlyphHoverWidget | null; private _isMouseDown: boolean; private _hoverClicked: boolean; @@ -64,15 +49,16 @@ export class ModesHoverController implements IEditorContribution { } constructor(private readonly _editor: ICodeEditor, + @IInstantiationService private readonly _instantiationService: IInstantiationService, @IOpenerService private readonly _openerService: IOpenerService, @IModeService private readonly _modeService: IModeService, - @IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, @IThemeService private readonly _themeService: IThemeService, @IContextKeyService _contextKeyService: IContextKeyService ) { this._isMouseDown = false; this._hoverClicked = false; + this._contentWidget = null; + this._glyphWidget = null; this._hookEvents(); @@ -113,8 +99,8 @@ export class ModesHoverController implements IEditorContribution { } private _onModelDecorationsChanged(): void { - this.contentWidget.onModelDecorationsChanged(); - this.glyphWidget.onModelDecorationsChanged(); + this._contentWidget?.onModelDecorationsChanged(); + this._glyphWidget?.onModelDecorationsChanged(); } private _onEditorScrollChanged(e: IScrollEvent): void { @@ -153,7 +139,7 @@ export class ModesHoverController implements IEditorContribution { private _onEditorMouseMove(mouseEvent: IEditorMouseEvent): void { let targetType = mouseEvent.target.type; - if (this._isMouseDown && this._hoverClicked && this.contentWidget.isColorPickerVisible()) { + if (this._isMouseDown && this._hoverClicked) { return; } @@ -162,10 +148,14 @@ export class ModesHoverController implements IEditorContribution { return; } + if (this._isHoverSticky && !mouseEvent.event.browserEvent.view?.getSelection()?.isCollapsed) { + // selected text within content hover widget + return; + } if ( !this._isHoverSticky && targetType === MouseTargetType.CONTENT_WIDGET && mouseEvent.target.detail === ModesContentHoverWidget.ID - && this._contentWidget.value?.isColorPickerVisible() + && this._contentWidget?.isColorPickerVisible() ) { // though the hover is not sticky, the color picker needs to. return; @@ -186,26 +176,31 @@ export class ModesHoverController implements IEditorContribution { } if (targetType === MouseTargetType.CONTENT_TEXT) { - this.glyphWidget.hide(); + this._glyphWidget?.hide(); if (this._isHoverEnabled && mouseEvent.target.range) { // TODO@rebornix. This should be removed if we move Color Picker out of Hover component. // Check if mouse is hovering on color decorator const hoverOnColorDecorator = [...mouseEvent.target.element?.classList.values() || []].find(className => className.startsWith('ced-colorBox')) && mouseEvent.target.range.endColumn - mouseEvent.target.range.startColumn === 1; - if (hoverOnColorDecorator) { - // shift the mouse focus by one as color decorator is a `before` decoration of next character. - this.contentWidget.startShowingAt(new Range(mouseEvent.target.range.startLineNumber, mouseEvent.target.range.startColumn + 1, mouseEvent.target.range.endLineNumber, mouseEvent.target.range.endColumn + 1), HoverStartMode.Delayed, false); - } else { - this.contentWidget.startShowingAt(mouseEvent.target.range, HoverStartMode.Delayed, false); + const showAtRange = ( + hoverOnColorDecorator // shift the mouse focus by one as color decorator is a `before` decoration of next character. + ? new Range(mouseEvent.target.range.startLineNumber, mouseEvent.target.range.startColumn + 1, mouseEvent.target.range.endLineNumber, mouseEvent.target.range.endColumn + 1) + : mouseEvent.target.range + ); + if (!this._contentWidget) { + this._contentWidget = new ModesContentHoverWidget(this._editor, this._hoverVisibleKey, this._instantiationService, this._themeService); } - + this._contentWidget.startShowingAt(showAtRange, HoverStartMode.Delayed, false); } } else if (targetType === MouseTargetType.GUTTER_GLYPH_MARGIN) { - this.contentWidget.hide(); + this._contentWidget?.hide(); if (this._isHoverEnabled && mouseEvent.target.position) { - this.glyphWidget.startShowingAt(mouseEvent.target.position.lineNumber); + if (!this._glyphWidget) { + this._glyphWidget = new ModesGlyphHoverWidget(this._editor, this._modeService, this._openerService); + } + this._glyphWidget.startShowingAt(mouseEvent.target.position.lineNumber); } } else { this._hideWidgets(); @@ -220,29 +215,32 @@ export class ModesHoverController implements IEditorContribution { } private _hideWidgets(): void { - if (!this._glyphWidget.value || !this._contentWidget.value || (this._isMouseDown && this._hoverClicked && this._contentWidget.value.isColorPickerVisible())) { + if ((this._isMouseDown && this._hoverClicked && this._contentWidget?.isColorPickerVisible())) { return; } - this._glyphWidget.value.hide(); - this._contentWidget.value.hide(); + this._hoverClicked = false; + this._glyphWidget?.hide(); + this._contentWidget?.hide(); } - private _createHoverWidgets() { - this._contentWidget.value = new ModesContentHoverWidget(this._editor, this._hoverVisibleKey, this._markerDecorationsService, this._keybindingService, this._themeService, this._modeService, this._openerService); - this._glyphWidget.value = new ModesGlyphHoverWidget(this._editor, this._modeService, this._openerService); + public isColorPickerVisible(): boolean { + return this._contentWidget?.isColorPickerVisible() || false; } public showContentHover(range: Range, mode: HoverStartMode, focus: boolean): void { - this.contentWidget.startShowingAt(range, mode, focus); + if (!this._contentWidget) { + this._contentWidget = new ModesContentHoverWidget(this._editor, this._hoverVisibleKey, this._instantiationService, this._themeService); + } + this._contentWidget.startShowingAt(range, mode, focus); } public dispose(): void { this._unhookEvents(); this._toUnhook.dispose(); this._didChangeConfigurationHandler.dispose(); - this._glyphWidget.dispose(); - this._contentWidget.dispose(); + this._glyphWidget?.dispose(); + this._contentWidget?.dispose(); } } diff --git a/src/vs/editor/contrib/hover/hoverWidgets.ts b/src/vs/editor/contrib/hover/hoverWidgets.ts index cb8c0280..ecc374f2 100644 --- a/src/vs/editor/contrib/hover/hoverWidgets.ts +++ b/src/vs/editor/contrib/hover/hoverWidgets.ts @@ -3,168 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Widget } from 'vs/base/browser/ui/widget'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { IContentWidget, ICodeEditor, IContentWidgetPosition, ContentWidgetPositionPreference, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; -import { Position } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; -import { renderHoverAction, HoverWidget } from 'vs/base/browser/ui/hover/hoverWidget'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; - -export class ContentHoverWidget extends Widget implements IContentWidget { - - protected readonly _hover: HoverWidget; - private readonly _id: string; - protected _editor: ICodeEditor; - private _isVisible: boolean; - protected _showAtPosition: Position | null; - protected _showAtRange: Range | null; - private _stoleFocus: boolean; - - // Editor.IContentWidget.allowEditorOverflow - public allowEditorOverflow = true; - - protected get isVisible(): boolean { - return this._isVisible; - } - - protected set isVisible(value: boolean) { - this._isVisible = value; - this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible); - } - - constructor( - id: string, - editor: ICodeEditor, - private readonly _hoverVisibleKey: IContextKey, - private readonly _keybindingService: IKeybindingService - ) { - super(); - - this._hover = this._register(new HoverWidget()); - this._id = id; - this._editor = editor; - this._isVisible = false; - this._stoleFocus = false; - - this.onkeydown(this._hover.containerDomNode, (e: IKeyboardEvent) => { - if (e.equals(KeyCode.Escape)) { - this.hide(); - } - }); - - this._register(this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { - if (e.hasChanged(EditorOption.fontInfo)) { - this.updateFont(); - } - })); - - this._editor.onDidLayoutChange(e => this.layout()); - - this.layout(); - this._editor.addContentWidget(this); - this._showAtPosition = null; - this._showAtRange = null; - this._stoleFocus = false; - } - - public getId(): string { - return this._id; - } - - public getDomNode(): HTMLElement { - return this._hover.containerDomNode; - } - - public showAt(position: Position, range: Range | null, focus: boolean): void { - // Position has changed - this._showAtPosition = position; - this._showAtRange = range; - this._hoverVisibleKey.set(true); - this.isVisible = true; - - this._editor.layoutContentWidget(this); - // Simply force a synchronous render on the editor - // such that the widget does not really render with left = '0px' - this._editor.render(); - this._stoleFocus = focus; - if (focus) { - this._hover.containerDomNode.focus(); - } - } - - public hide(): void { - if (!this.isVisible) { - return; - } - - setTimeout(() => { - // Give commands a chance to see the key - if (!this.isVisible) { - this._hoverVisibleKey.set(false); - } - }, 0); - this.isVisible = false; - - this._editor.layoutContentWidget(this); - if (this._stoleFocus) { - this._editor.focus(); - } - } - - public getPosition(): IContentWidgetPosition | null { - if (this.isVisible) { - return { - position: this._showAtPosition, - range: this._showAtRange, - preference: [ - ContentWidgetPositionPreference.ABOVE, - ContentWidgetPositionPreference.BELOW - ] - }; - } - return null; - } - - public dispose(): void { - this._editor.removeContentWidget(this); - super.dispose(); - } - - private updateFont(): void { - const codeClasses: HTMLElement[] = Array.prototype.slice.call(this._hover.contentsDomNode.getElementsByClassName('code')); - codeClasses.forEach(node => this._editor.applyFontInfo(node)); - } - - protected updateContents(node: Node): void { - this._hover.contentsDomNode.textContent = ''; - this._hover.contentsDomNode.appendChild(node); - this.updateFont(); - - this._editor.layoutContentWidget(this); - this._hover.onContentsChanged(); - } - - protected _renderAction(parent: HTMLElement, actionOptions: { label: string, iconClass?: string, run: (target: HTMLElement) => void, commandId: string }): IDisposable { - const keybinding = this._keybindingService.lookupKeybinding(actionOptions.commandId); - const keybindingLabel = keybinding ? keybinding.getLabel() : null; - return renderHoverAction(parent, actionOptions, keybindingLabel); - } - - private layout(): void { - const height = Math.max(this._editor.getLayoutInfo().height / 4, 250); - const { fontSize, lineHeight } = this._editor.getOption(EditorOption.fontInfo); - - this._hover.contentsDomNode.style.fontSize = `${fontSize}px`; - this._hover.contentsDomNode.style.lineHeight = `${lineHeight}px`; - this._hover.contentsDomNode.style.maxHeight = `${height}px`; - this._hover.contentsDomNode.style.maxWidth = `${Math.max(this._editor.getLayoutInfo().width * 0.66, 500)}px`; - } -} export class GlyphHoverWidget extends Widget implements IOverlayWidget { diff --git a/src/vs/editor/contrib/hover/markdownHoverParticipant.ts b/src/vs/editor/contrib/hover/markdownHoverParticipant.ts new file mode 100644 index 00000000..ec4442db --- /dev/null +++ b/src/vs/editor/contrib/hover/markdownHoverParticipant.ts @@ -0,0 +1,126 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import * as dom from 'vs/base/browser/dom'; +import { IMarkdownString, MarkdownString, isEmptyMarkdownString, markedStringsEquals } from 'vs/base/common/htmlContent'; +import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { asArray } from 'vs/base/common/arrays'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { IModelDecoration } from 'vs/editor/common/model'; +import { IEditorHover, IEditorHoverParticipant, IHoverPart } from 'vs/editor/contrib/hover/modesContentHover'; +import { HoverProviderRegistry } from 'vs/editor/common/modes'; +import { getHover } from 'vs/editor/contrib/hover/getHover'; +import { Position } from 'vs/editor/common/core/position'; +import { CancellationToken } from 'vs/base/common/cancellation'; + +const $ = dom.$; + +export class MarkdownHover implements IHoverPart { + + constructor( + public readonly range: Range, + public readonly contents: IMarkdownString[] + ) { } + + public equals(other: IHoverPart): boolean { + if (other instanceof MarkdownHover) { + return markedStringsEquals(this.contents, other.contents); + } + return false; + } +} + +export class MarkdownHoverParticipant implements IEditorHoverParticipant { + + constructor( + private readonly _editor: ICodeEditor, + private readonly _hover: IEditorHover, + @IModeService private readonly _modeService: IModeService, + @IOpenerService private readonly _openerService: IOpenerService, + ) { } + + public createLoadingMessage(range: Range): MarkdownHover { + return new MarkdownHover(range, [new MarkdownString().appendText(nls.localize('modesContentHover.loading', "Loading..."))]); + } + + public computeSync(hoverRange: Range, lineDecorations: IModelDecoration[]): MarkdownHover[] { + if (!this._editor.hasModel()) { + return []; + } + + const model = this._editor.getModel(); + const lineNumber = hoverRange.startLineNumber; + const maxColumn = model.getLineMaxColumn(lineNumber); + const result: MarkdownHover[] = []; + for (const d of lineDecorations) { + const startColumn = (d.range.startLineNumber === lineNumber) ? d.range.startColumn : 1; + const endColumn = (d.range.endLineNumber === lineNumber) ? d.range.endColumn : maxColumn; + + const hoverMessage = d.options.hoverMessage; + if (!hoverMessage || isEmptyMarkdownString(hoverMessage)) { + continue; + } + + const range = new Range(hoverRange.startLineNumber, startColumn, hoverRange.startLineNumber, endColumn); + result.push(new MarkdownHover(range, asArray(hoverMessage))); + } + + return result; + } + + public async computeAsync(range: Range, token: CancellationToken): Promise { + if (!this._editor.hasModel() || !range) { + return Promise.resolve([]); + } + + const model = this._editor.getModel(); + + if (!HoverProviderRegistry.has(model)) { + return Promise.resolve([]); + } + + const hovers = await getHover(model, new Position( + range.startLineNumber, + range.startColumn + ), token); + + const result: MarkdownHover[] = []; + for (const hover of hovers) { + if (isEmptyMarkdownString(hover.contents)) { + continue; + } + const rng = hover.range ? Range.lift(hover.range) : range; + result.push(new MarkdownHover(rng, hover.contents)); + } + return result; + } + + public renderHoverParts(hoverParts: MarkdownHover[], fragment: DocumentFragment): IDisposable { + const disposables = new DisposableStore(); + for (const hoverPart of hoverParts) { + for (const contents of hoverPart.contents) { + if (isEmptyMarkdownString(contents)) { + continue; + } + const markdownHoverElement = $('div.hover-row.markdown-hover'); + const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents')); + const renderer = disposables.add(new MarkdownRenderer({ editor: this._editor }, this._modeService, this._openerService)); + disposables.add(renderer.onDidRenderAsync(() => { + hoverContentsElement.className = 'hover-contents code-hover-contents'; + this._hover.onContentsChanged(); + })); + const renderedContents = disposables.add(renderer.render(contents)); + hoverContentsElement.appendChild(renderedContents.element); + fragment.appendChild(markdownHoverElement); + } + } + return disposables; + } +} diff --git a/src/vs/editor/contrib/hover/markerHoverParticipant.ts b/src/vs/editor/contrib/hover/markerHoverParticipant.ts new file mode 100644 index 00000000..ad739c89 --- /dev/null +++ b/src/vs/editor/contrib/hover/markerHoverParticipant.ts @@ -0,0 +1,257 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import * as dom from 'vs/base/browser/dom'; +import { IDisposable, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import { CodeActionTriggerType } from 'vs/editor/common/modes'; +import { isNonEmptyArray } from 'vs/base/common/arrays'; +import { IMarker, IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { basename } from 'vs/base/common/resources'; +import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDecorationService'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { MarkerController, NextMarkerAction } from 'vs/editor/contrib/gotoError/gotoError'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { CancelablePromise, createCancelablePromise, disposableTimeout } 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, CodeActionTrigger } from 'vs/editor/contrib/codeAction/types'; +import { IModelDecoration } from 'vs/editor/common/model'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Progress } from 'vs/platform/progress/common/progress'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { renderHoverAction } from 'vs/base/browser/ui/hover/hoverWidget'; +import { IEditorHover, IEditorHoverParticipant, IHoverPart } from 'vs/editor/contrib/hover/modesContentHover'; + +const $ = dom.$; + +export class MarkerHover implements IHoverPart { + + constructor( + public readonly range: Range, + public readonly marker: IMarker, + ) { } + + public equals(other: IHoverPart): boolean { + if (other instanceof MarkerHover) { + return IMarkerData.makeKey(this.marker) === IMarkerData.makeKey(other.marker); + } + return false; + } +} + +const markerCodeActionTrigger: CodeActionTrigger = { + type: CodeActionTriggerType.Manual, + filter: { include: CodeActionKind.QuickFix } +}; + +export class MarkerHoverParticipant implements IEditorHoverParticipant { + + private recentMarkerCodeActionsInfo: { marker: IMarker, hasCodeActions: boolean } | undefined = undefined; + + constructor( + private readonly _editor: ICodeEditor, + private readonly _hover: IEditorHover, + @IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IOpenerService private readonly _openerService: IOpenerService, + ) { } + + public computeSync(hoverRange: Range, lineDecorations: IModelDecoration[]): MarkerHover[] { + if (!this._editor.hasModel()) { + return []; + } + + const model = this._editor.getModel(); + const lineNumber = hoverRange.startLineNumber; + const maxColumn = model.getLineMaxColumn(lineNumber); + const result: MarkerHover[] = []; + for (const d of lineDecorations) { + const startColumn = (d.range.startLineNumber === lineNumber) ? d.range.startColumn : 1; + const endColumn = (d.range.endLineNumber === lineNumber) ? d.range.endColumn : maxColumn; + + const marker = this._markerDecorationsService.getMarker(model.uri, d); + if (!marker) { + continue; + } + + const range = new Range(hoverRange.startLineNumber, startColumn, hoverRange.startLineNumber, endColumn); + result.push(new MarkerHover(range, marker)); + } + + return result; + } + + public renderHoverParts(hoverParts: MarkerHover[], fragment: DocumentFragment): IDisposable { + if (!hoverParts.length) { + return Disposable.None; + } + const disposables = new DisposableStore(); + hoverParts.forEach(msg => fragment.appendChild(this.renderMarkerHover(msg, disposables))); + const markerHoverForStatusbar = hoverParts.length === 1 ? hoverParts[0] : hoverParts.sort((a, b) => MarkerSeverity.compare(a.marker.severity, b.marker.severity))[0]; + fragment.appendChild(this.renderMarkerStatusbar(markerHoverForStatusbar, disposables)); + return disposables; + } + + private renderMarkerHover(markerHover: MarkerHover, disposables: DisposableStore): HTMLElement { + const hoverElement = $('div.hover-row'); + const markerElement = dom.append(hoverElement, $('div.marker.hover-contents')); + const { source, message, code, relatedInformation } = markerHover.marker; + + this._editor.applyFontInfo(markerElement); + const messageElement = dom.append(markerElement, $('span')); + messageElement.style.whiteSpace = 'pre-wrap'; + messageElement.innerText = message; + + if (source || code) { + // Code has link + if (code && typeof code !== 'string') { + const sourceAndCodeElement = $('span'); + if (source) { + const sourceElement = dom.append(sourceAndCodeElement, $('span')); + sourceElement.innerText = source; + } + const codeLink = dom.append(sourceAndCodeElement, $('a.code-link')); + codeLink.setAttribute('href', code.target.toString()); + + disposables.add(dom.addDisposableListener(codeLink, 'click', (e) => { + this._openerService.open(code.target); + e.preventDefault(); + e.stopPropagation(); + })); + + const codeElement = dom.append(codeLink, $('span')); + codeElement.innerText = code.value; + + const detailsElement = dom.append(markerElement, sourceAndCodeElement); + detailsElement.style.opacity = '0.6'; + detailsElement.style.paddingLeft = '6px'; + } else { + const detailsElement = dom.append(markerElement, $('span')); + detailsElement.style.opacity = '0.6'; + detailsElement.style.paddingLeft = '6px'; + detailsElement.innerText = source && code ? `${source}(${code})` : source ? source : `(${code})`; + } + } + + if (isNonEmptyArray(relatedInformation)) { + for (const { message, resource, startLineNumber, startColumn } of relatedInformation) { + const relatedInfoContainer = dom.append(markerElement, $('div')); + relatedInfoContainer.style.marginTop = '8px'; + const a = dom.append(relatedInfoContainer, $('a')); + a.innerText = `${basename(resource)}(${startLineNumber}, ${startColumn}): `; + a.style.cursor = 'pointer'; + disposables.add(dom.addDisposableListener(a, 'click', (e) => { + e.stopPropagation(); + e.preventDefault(); + if (this._openerService) { + this._openerService.open(resource, { + fromUserGesture: true, + editorOptions: { selection: { startLineNumber, startColumn } } + }).catch(onUnexpectedError); + } + })); + const messageElement = dom.append(relatedInfoContainer, $('span')); + messageElement.innerText = message; + this._editor.applyFontInfo(messageElement); + } + } + + return hoverElement; + } + + private renderMarkerStatusbar(markerHover: MarkerHover, disposables: DisposableStore): HTMLElement { + const hoverElement = $('div.hover-row.status-bar'); + const actionsElement = dom.append(hoverElement, $('div.actions')); + if (markerHover.marker.severity === MarkerSeverity.Error || markerHover.marker.severity === MarkerSeverity.Warning || markerHover.marker.severity === MarkerSeverity.Info) { + disposables.add(this.renderAction(actionsElement, { + label: nls.localize('peek problem', "Peek Problem"), + commandId: NextMarkerAction.ID, + run: () => { + this._hover.hide(); + MarkerController.get(this._editor).showAtMarker(markerHover.marker); + this._editor.focus(); + } + })); + } + + if (!this._editor.getOption(EditorOption.readOnly)) { + const quickfixPlaceholderElement = dom.append(actionsElement, $('div')); + if (this.recentMarkerCodeActionsInfo) { + if (IMarkerData.makeKey(this.recentMarkerCodeActionsInfo.marker) === IMarkerData.makeKey(markerHover.marker)) { + if (!this.recentMarkerCodeActionsInfo.hasCodeActions) { + quickfixPlaceholderElement.textContent = nls.localize('noQuickFixes', "No quick fixes available"); + } + } else { + this.recentMarkerCodeActionsInfo = undefined; + } + } + const updatePlaceholderDisposable = this.recentMarkerCodeActionsInfo && !this.recentMarkerCodeActionsInfo.hasCodeActions ? Disposable.None : disposables.add(disposableTimeout(() => quickfixPlaceholderElement.textContent = nls.localize('checkingForQuickFixes', "Checking for quick fixes..."), 200)); + if (!quickfixPlaceholderElement.textContent) { + // Have some content in here to avoid flickering + quickfixPlaceholderElement.textContent = String.fromCharCode(0xA0); //   + } + const codeActionsPromise = this.getCodeActions(markerHover.marker); + disposables.add(toDisposable(() => codeActionsPromise.cancel())); + codeActionsPromise.then(actions => { + updatePlaceholderDisposable.dispose(); + this.recentMarkerCodeActionsInfo = { marker: markerHover.marker, hasCodeActions: actions.validActions.length > 0 }; + + if (!this.recentMarkerCodeActionsInfo.hasCodeActions) { + actions.dispose(); + quickfixPlaceholderElement.textContent = nls.localize('noQuickFixes', "No quick fixes available"); + return; + } + quickfixPlaceholderElement.style.display = 'none'; + + let showing = false; + disposables.add(toDisposable(() => { + if (!showing) { + actions.dispose(); + } + })); + + disposables.add(this.renderAction(actionsElement, { + label: nls.localize('quick fixes', "Quick Fix..."), + commandId: QuickFixAction.Id, + run: (target) => { + showing = true; + const controller = QuickFixController.get(this._editor); + const elementPosition = dom.getDomNodePagePosition(target); + // Hide the hover pre-emptively, otherwise the editor can close the code actions + // context menu as well when using keyboard navigation + this._hover.hide(); + controller.showCodeActions(markerCodeActionTrigger, actions, { + x: elementPosition.left + 6, + y: elementPosition.top + elementPosition.height + 6 + }); + } + })); + }); + } + + return hoverElement; + } + + private renderAction(parent: HTMLElement, actionOptions: { label: string, iconClass?: string, run: (target: HTMLElement) => void, commandId: string }): IDisposable { + const keybinding = this._keybindingService.lookupKeybinding(actionOptions.commandId); + const keybindingLabel = keybinding ? keybinding.getLabel() : null; + return renderHoverAction(parent, actionOptions, keybindingLabel); + } + + private getCodeActions(marker: IMarker): CancelablePromise { + return createCancelablePromise(cancellationToken => { + return getCodeActions( + this._editor.getModel()!, + new Range(marker.startLineNumber, marker.startColumn, marker.endLineNumber, marker.endColumn), + markerCodeActionTrigger, + Progress.None, + cancellationToken); + }); + } +} diff --git a/src/vs/editor/contrib/hover/modesContentHover.ts b/src/vs/editor/contrib/hover/modesContentHover.ts index ad2d6b27..ee10d9b8 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -3,228 +3,242 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Color, RGBA } from 'vs/base/common/color'; -import { IMarkdownString, MarkdownString, isEmptyMarkdownString, markedStringsEquals } from 'vs/base/common/htmlContent'; -import { IDisposable, toDisposable, DisposableStore, combinedDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IDisposable, DisposableStore, combinedDisposable } 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 { Range } from 'vs/editor/common/core/range'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { DocumentColorProvider, Hover as MarkdownHover, HoverProviderRegistry, IColor, TokenizationRegistry, CodeActionTriggerType } from 'vs/editor/common/modes'; +import { DocumentColorProvider, IColor, TokenizationRegistry } 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'; import { ColorPickerWidget } from 'vs/editor/contrib/colorPicker/colorPickerWidget'; -import { getHover } from 'vs/editor/contrib/hover/getHover'; import { HoverOperation, HoverStartMode, IHoverComputer } from 'vs/editor/contrib/hover/hoverOperation'; -import { ContentHoverWidget } from 'vs/editor/contrib/hover/hoverWidgets'; -import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { coalesce, isNonEmptyArray, asArray } from 'vs/base/common/arrays'; -import { IMarker, IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; -import { basename } from 'vs/base/common/resources'; -import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDecorationService'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { IOpenerService, NullOpenerService } from 'vs/platform/opener/common/opener'; -import { MarkerController, NextMarkerAction } from 'vs/editor/contrib/gotoError/gotoError'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { CancelablePromise, createCancelablePromise, disposableTimeout } 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, CodeActionTrigger } from 'vs/editor/contrib/codeAction/types'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { IIdentifiedSingleEditOperation, TrackedRangeStickiness } from 'vs/editor/common/model'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { coalesce } from 'vs/base/common/arrays'; +import { IIdentifiedSingleEditOperation, IModelDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import { Constants } from 'vs/base/common/uint'; import { textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; -import { Progress } from 'vs/platform/progress/common/progress'; import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { Widget } from 'vs/base/browser/ui/widget'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { HoverWidget } from 'vs/base/browser/ui/hover/hoverWidget'; +import { MarkerHover, MarkerHoverParticipant } from 'vs/editor/contrib/hover/markerHoverParticipant'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { MarkdownHover, MarkdownHoverParticipant } from 'vs/editor/contrib/hover/markdownHoverParticipant'; -const $ = dom.$; +export interface IHoverPart { + readonly range: Range; + equals(other: IHoverPart): boolean; +} -class ColorHover { +export interface IEditorHover { + hide(): void; + onContentsChanged(): void; +} + +export interface IEditorHoverParticipant { + computeSync(hoverRange: Range, lineDecorations: IModelDecoration[]): T[]; + computeAsync?(range: Range, token: CancellationToken): Promise; + renderHoverParts(hoverParts: T[], fragment: DocumentFragment): IDisposable; +} + +class ColorHover implements IHoverPart { constructor( - public readonly range: IRange, + public readonly range: Range, public readonly color: IColor, public readonly provider: DocumentColorProvider ) { } + + equals(other: IHoverPart): boolean { + return false; + } } -class MarkerHover { - +class HoverPartInfo { constructor( - public readonly range: IRange, - public readonly marker: IMarker, + public readonly owner: IEditorHoverParticipant | null, + public readonly data: IHoverPart ) { } } -type HoverPart = MarkdownHover | ColorHover | MarkerHover; - -class ModesContentComputer implements IHoverComputer { +class ModesContentComputer implements IHoverComputer { private readonly _editor: ICodeEditor; - private _result: HoverPart[]; - private _range?: Range; + private _result: HoverPartInfo[]; + private _range: Range | null; constructor( editor: ICodeEditor, - private readonly _markerDecorationsService: IMarkerDecorationsService + private readonly _markerHoverParticipant: IEditorHoverParticipant, + private readonly _markdownHoverParticipant: MarkdownHoverParticipant ) { this._editor = editor; this._result = []; + this._range = null; } - setRange(range: Range): void { + public setRange(range: Range): void { this._range = range; this._result = []; } - clearResult(): void { + public clearResult(): void { this._result = []; } - computeAsync(token: CancellationToken): Promise { + public async computeAsync(token: CancellationToken): Promise { if (!this._editor.hasModel() || !this._range) { return Promise.resolve([]); } - const model = this._editor.getModel(); - - if (!HoverProviderRegistry.has(model)) { - return Promise.resolve([]); - } - - return getHover(model, new Position( - this._range.startLineNumber, - this._range.startColumn - ), token); + const markdownHovers = await this._markdownHoverParticipant.computeAsync(this._range, token); + return markdownHovers.map(h => new HoverPartInfo(this._markdownHoverParticipant, h)); } - computeSync(): HoverPart[] { + public computeSync(): HoverPartInfo[] { if (!this._editor.hasModel() || !this._range) { return []; } const model = this._editor.getModel(); - const lineNumber = this._range.startLineNumber; + const hoverRange = this._range; + const lineNumber = hoverRange.startLineNumber; if (lineNumber > this._editor.getModel().getLineCount()) { // Illegal line number => no results return []; } - const colorDetector = ColorDetector.get(this._editor); const maxColumn = model.getLineMaxColumn(lineNumber); - const lineDecorations = this._editor.getLineDecorations(lineNumber); - let didFindColor = false; - - const hoverRange = this._range; - const result = lineDecorations.map((d): HoverPart | null => { + const lineDecorations = this._editor.getLineDecorations(lineNumber).filter((d) => { const startColumn = (d.range.startLineNumber === lineNumber) ? d.range.startColumn : 1; const endColumn = (d.range.endLineNumber === lineNumber) ? d.range.endColumn : maxColumn; - if (startColumn > hoverRange.startColumn || hoverRange.endColumn > endColumn) { - return null; - } - - const range = new Range(hoverRange.startLineNumber, startColumn, hoverRange.startLineNumber, endColumn); - const marker = this._markerDecorationsService.getMarker(model, d); - if (marker) { - return new MarkerHover(range, marker); - } - - const colorData = colorDetector.getColorData(d.range.getStartPosition()); - - if (!didFindColor && colorData) { - didFindColor = true; - - const { color, range } = colorData.colorInfo; - return new ColorHover(range, color, colorData.provider); - } else { - if (isEmptyMarkdownString(d.options.hoverMessage)) { - return null; - } - - const contents: IMarkdownString[] = d.options.hoverMessage ? asArray(d.options.hoverMessage) : []; - return { contents, range }; + return false; } + return true; }); + let result: HoverPartInfo[] = []; + + const colorDetector = ColorDetector.get(this._editor); + for (const d of lineDecorations) { + const colorData = colorDetector.getColorData(d.range.getStartPosition()); + if (colorData) { + const { color, range } = colorData.colorInfo; + result.push(new HoverPartInfo(null, new ColorHover(Range.lift(range), color, colorData.provider))); + break; + } + } + + const markdownHovers = this._markdownHoverParticipant.computeSync(this._range, lineDecorations); + result = result.concat(markdownHovers.map(h => new HoverPartInfo(this._markdownHoverParticipant, h))); + + const markerHovers = this._markerHoverParticipant.computeSync(this._range, lineDecorations); + result = result.concat(markerHovers.map(h => new HoverPartInfo(this._markerHoverParticipant, h))); + return coalesce(result); } - onResult(result: HoverPart[], isFromSynchronousComputation: boolean): void { + public onResult(result: HoverPartInfo[], isFromSynchronousComputation: boolean): void { // Always put synchronous messages before asynchronous ones if (isFromSynchronousComputation) { - this._result = result.concat(this._result.sort((a, b) => { - if (a instanceof ColorHover) { // sort picker messages at to the top - return -1; - } else if (b instanceof ColorHover) { - return 1; - } - return 0; - })); + this._result = result.concat(this._result); } else { this._result = this._result.concat(result); } } - getResult(): HoverPart[] { + public getResult(): HoverPartInfo[] { return this._result.slice(0); } - getResultWithLoadingMessage(): HoverPart[] { - return this._result.slice(0).concat([this._getLoadingMessage()]); - } + public getResultWithLoadingMessage(): HoverPartInfo[] { + if (this._range) { + const loadingMessage = new HoverPartInfo(this._markdownHoverParticipant, this._markdownHoverParticipant.createLoadingMessage(this._range)); + return this._result.slice(0).concat([loadingMessage]); - private _getLoadingMessage(): HoverPart { - return { - range: this._range, - contents: [new MarkdownString().appendText(nls.localize('modesContentHover.loading', "Loading..."))] - }; + } + return this._result.slice(0); } } -const markerCodeActionTrigger: CodeActionTrigger = { - type: CodeActionTriggerType.Manual, - filter: { include: CodeActionKind.QuickFix } -}; - -export class ModesContentHoverWidget extends ContentHoverWidget { +export class ModesContentHoverWidget extends Widget implements IContentWidget, IEditorHover { static readonly ID = 'editor.contrib.modesContentHoverWidget'; - private _messages: HoverPart[]; + private readonly _markerHoverParticipant: IEditorHoverParticipant; + private readonly _markdownHoverParticipant: MarkdownHoverParticipant; + + private readonly _hover: HoverWidget; + private readonly _id: string; + private readonly _editor: ICodeEditor; + private _isVisible: boolean; + private _showAtPosition: Position | null; + private _showAtRange: Range | null; + private _stoleFocus: boolean; + + // IContentWidget.allowEditorOverflow + public readonly allowEditorOverflow = true; + + private _messages: HoverPartInfo[]; private _lastRange: Range | null; private readonly _computer: ModesContentComputer; - private readonly _hoverOperation: HoverOperation; + private readonly _hoverOperation: HoverOperation; private _highlightDecorations: string[]; private _isChangingDecorations: boolean; private _shouldFocus: boolean; private _colorPicker: ColorPickerWidget | null; - - private _codeLink?: HTMLElement; - - private readonly renderDisposable = this._register(new MutableDisposable()); + private _renderDisposable: IDisposable | null; constructor( editor: ICodeEditor, - _hoverVisibleKey: IContextKey, - markerDecorationsService: IMarkerDecorationsService, - keybindingService: IKeybindingService, + private readonly _hoverVisibleKey: IContextKey, + instantiationService: IInstantiationService, private readonly _themeService: IThemeService, - private readonly _modeService: IModeService, - private readonly _openerService: IOpenerService = NullOpenerService, ) { - super(ModesContentHoverWidget.ID, editor, _hoverVisibleKey, keybindingService); + super(); + + this._markerHoverParticipant = instantiationService.createInstance(MarkerHoverParticipant, editor, this); + this._markdownHoverParticipant = instantiationService.createInstance(MarkdownHoverParticipant, editor, this); + + this._hover = this._register(new HoverWidget()); + this._id = ModesContentHoverWidget.ID; + this._editor = editor; + this._isVisible = false; + this._stoleFocus = false; + this._renderDisposable = null; + + this.onkeydown(this._hover.containerDomNode, (e: IKeyboardEvent) => { + if (e.equals(KeyCode.Escape)) { + this.hide(); + } + }); + + this._register(this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { + if (e.hasChanged(EditorOption.fontInfo)) { + this._updateFont(); + } + })); + + this._editor.onDidLayoutChange(() => this.layout()); + + this.layout(); + this._editor.addContentWidget(this); + this._showAtPosition = null; + this._showAtRange = null; + this._stoleFocus = false; this._messages = []; this._lastRange = null; - this._computer = new ModesContentComputer(this._editor, markerDecorationsService); + this._computer = new ModesContentComputer(this._editor, this._markerHoverParticipant, this._markdownHoverParticipant); this._highlightDecorations = []; this._isChangingDecorations = false; this._shouldFocus = false; @@ -246,15 +260,15 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._register(dom.addStandardDisposableListener(this.getDomNode(), dom.EventType.BLUR, () => { this.getDomNode().classList.remove('colorpicker-hover'); })); - this._register(editor.onDidChangeConfiguration((e) => { + this._register(editor.onDidChangeConfiguration(() => { this._hoverOperation.setHoverTime(this._editor.getOption(EditorOption.hover).delay); })); - this._register(TokenizationRegistry.onDidChange((e) => { - if (this.isVisible && this._lastRange && this._messages.length > 0) { + this._register(TokenizationRegistry.onDidChange(() => { + if (this._isVisible && this._lastRange && this._messages.length > 0) { this._messages = this._messages.map(msg => { // If a color hover is visible, we need to update the message that // created it so that the color matches the last chosen color - if (msg instanceof ColorHover && !!this._lastRange?.intersectRanges(msg.range) && this._colorPicker?.model.color) { + if (msg.data instanceof ColorHover && !!this._lastRange?.intersectRanges(msg.data.range) && this._colorPicker?.model.color) { const color = this._colorPicker.model.color; const newColor = { red: color.rgba.r / 255, @@ -262,7 +276,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { blue: color.rgba.b / 255, alpha: color.rgba.a }; - return new ColorHover(msg.range, newColor, msg.provider); + return new HoverPartInfo(msg.owner, new ColorHover(msg.data.range, newColor, msg.data.provider)); } else { return msg; } @@ -274,16 +288,81 @@ export class ModesContentHoverWidget extends ContentHoverWidget { })); } - dispose(): void { + public dispose(): void { this._hoverOperation.cancel(); + this._editor.removeContentWidget(this); super.dispose(); } - onModelDecorationsChanged(): void { + public getId(): string { + return this._id; + } + + public getDomNode(): HTMLElement { + return this._hover.containerDomNode; + } + + public showAt(position: Position, range: Range | null, focus: boolean): void { + // Position has changed + this._showAtPosition = position; + this._showAtRange = range; + this._hoverVisibleKey.set(true); + this._isVisible = true; + this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible); + + this._editor.layoutContentWidget(this); + // Simply force a synchronous render on the editor + // such that the widget does not really render with left = '0px' + this._editor.render(); + this._stoleFocus = focus; + if (focus) { + this._hover.containerDomNode.focus(); + } + } + + public getPosition(): IContentWidgetPosition | null { + if (this._isVisible) { + return { + position: this._showAtPosition, + range: this._showAtRange, + preference: [ + ContentWidgetPositionPreference.ABOVE, + ContentWidgetPositionPreference.BELOW + ] + }; + } + return null; + } + + private _updateFont(): void { + const codeClasses: HTMLElement[] = Array.prototype.slice.call(this._hover.contentsDomNode.getElementsByClassName('code')); + codeClasses.forEach(node => this._editor.applyFontInfo(node)); + } + + private _updateContents(node: Node): void { + this._hover.contentsDomNode.textContent = ''; + this._hover.contentsDomNode.appendChild(node); + this._updateFont(); + + this._editor.layoutContentWidget(this); + this._hover.onContentsChanged(); + } + + private layout(): void { + const height = Math.max(this._editor.getLayoutInfo().height / 4, 250); + const { fontSize, lineHeight } = this._editor.getOption(EditorOption.fontInfo); + + this._hover.contentsDomNode.style.fontSize = `${fontSize}px`; + this._hover.contentsDomNode.style.lineHeight = `${lineHeight}px`; + this._hover.contentsDomNode.style.maxHeight = `${height}px`; + this._hover.contentsDomNode.style.maxWidth = `${Math.max(this._editor.getLayoutInfo().width * 0.66, 500)}px`; + } + + public onModelDecorationsChanged(): void { if (this._isChangingDecorations) { return; } - if (this.isVisible) { + if (this._isVisible) { // The decorations have changed and the hover is visible, // we need to recompute the displayed text this._hoverOperation.cancel(); @@ -295,7 +374,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { } } - startShowingAt(range: Range, mode: HoverStartMode, focus: boolean): void { + public startShowingAt(range: Range, mode: HoverStartMode, focus: boolean): void { if (this._lastRange && this._lastRange.equalsRange(range)) { // We have to show the widget at the exact same range as before, so no work is needed return; @@ -303,17 +382,17 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._hoverOperation.cancel(); - if (this.isVisible) { + if (this._isVisible) { // The range might have changed, but the hover is visible // Instead of hiding it completely, filter out messages that are still in the new range and // kick off a new computation if (!this._showAtPosition || this._showAtPosition.lineNumber !== range.startLineNumber) { this.hide(); } else { - let filteredMessages: HoverPart[] = []; + let filteredMessages: HoverPartInfo[] = []; for (let i = 0, len = this._messages.length; i < len; i++) { const msg = this._messages[i]; - const rng = msg.range; + const rng = msg.data.range; if (rng && rng.startColumn <= range.startColumn && rng.endColumn >= range.endColumn) { filteredMessages.push(msg); } @@ -335,25 +414,45 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._hoverOperation.start(mode); } - hide(): void { + public hide(): void { this._lastRange = null; this._hoverOperation.cancel(); - super.hide(); + + if (this._isVisible) { + setTimeout(() => { + // Give commands a chance to see the key + if (!this._isVisible) { + this._hoverVisibleKey.set(false); + } + }, 0); + this._isVisible = false; + this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible); + + this._editor.layoutContentWidget(this); + if (this._stoleFocus) { + this._editor.focus(); + } + } + this._isChangingDecorations = true; this._highlightDecorations = this._editor.deltaDecorations(this._highlightDecorations, []); this._isChangingDecorations = false; - this.renderDisposable.clear(); + if (this._renderDisposable) { + this._renderDisposable.dispose(); + this._renderDisposable = null; + } this._colorPicker = null; } - isColorPickerVisible(): boolean { - if (this._colorPicker) { - return true; - } - return false; + public isColorPickerVisible(): boolean { + return !!this._colorPicker; } - private _withResult(result: HoverPart[], complete: boolean): void { + public onContentsChanged(): void { + this._hover.onContentsChanged(); + } + + private _withResult(result: HoverPartInfo[], complete: boolean): void { this._messages = result; if (this._lastRange && this._messages.length > 0) { @@ -363,20 +462,24 @@ export class ModesContentHoverWidget extends ContentHoverWidget { } } - private _renderMessages(renderRange: Range, messages: HoverPart[]): void { - this.renderDisposable.dispose(); + private _renderMessages(renderRange: Range, messages: HoverPartInfo[]): void { + if (this._renderDisposable) { + this._renderDisposable.dispose(); + this._renderDisposable = null; + } this._colorPicker = null; // update column from which to show let renderColumn = Constants.MAX_SAFE_SMALL_INTEGER; - let highlightRange: Range | null = messages[0].range ? Range.lift(messages[0].range) : null; + let highlightRange: Range | null = messages[0].data.range ? Range.lift(messages[0].data.range) : null; let fragment = document.createDocumentFragment(); - let isEmptyHoverContent = true; let containColorPicker = false; - const markdownDisposeables = new DisposableStore(); + const disposables = new DisposableStore(); const markerMessages: MarkerHover[] = []; - messages.forEach((msg) => { + const markdownParts: MarkdownHover[] = []; + messages.forEach((_msg) => { + const msg = _msg.data; if (!msg.range) { return; } @@ -464,46 +567,37 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._colorPicker = widget; this.showAt(range.getStartPosition(), range, this._shouldFocus); - this.updateContents(fragment); + this._updateContents(fragment); this._colorPicker.layout(); - this.renderDisposable.value = combinedDisposable(colorListener, colorChangeListener, widget, markdownDisposeables); + this._renderDisposable = combinedDisposable(colorListener, colorChangeListener, widget, disposables); }); } else { if (msg instanceof MarkerHover) { markerMessages.push(msg); - isEmptyHoverContent = false; } else { - msg.contents - .filter(contents => !isEmptyMarkdownString(contents)) - .forEach(contents => { - const markdownHoverElement = $('div.hover-row.markdown-hover'); - const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents')); - const renderer = markdownDisposeables.add(new MarkdownRenderer({ editor: this._editor }, this._modeService, this._openerService)); - markdownDisposeables.add(renderer.onDidRenderAsync(() => { - hoverContentsElement.className = 'hover-contents code-hover-contents'; - this._hover.onContentsChanged(); - })); - const renderedContents = markdownDisposeables.add(renderer.render(contents)); - hoverContentsElement.appendChild(renderedContents.element); - fragment.appendChild(markdownHoverElement); - isEmptyHoverContent = false; - }); + if (msg instanceof MarkdownHover) { + markdownParts.push(msg); + } } } }); - if (markerMessages.length) { - markerMessages.forEach(msg => fragment.appendChild(this.renderMarkerHover(msg))); - const markerHoverForStatusbar = markerMessages.length === 1 ? markerMessages[0] : markerMessages.sort((a, b) => MarkerSeverity.compare(a.marker.severity, b.marker.severity))[0]; - fragment.appendChild(this.renderMarkerStatusbar(markerHoverForStatusbar)); + if (markdownParts.length > 0) { + disposables.add(this._markdownHoverParticipant.renderHoverParts(markdownParts, fragment)); } + if (markerMessages.length) { + disposables.add(this._markerHoverParticipant.renderHoverParts(markerMessages, fragment)); + } + + this._renderDisposable = disposables; + // show - if (!containColorPicker && !isEmptyHoverContent) { + if (!containColorPicker && fragment.hasChildNodes()) { this.showAt(new Position(renderRange.startLineNumber, renderColumn), highlightRange, this._shouldFocus); - this.updateContents(fragment); + this._updateContents(fragment); } this._isChangingDecorations = true; @@ -514,175 +608,17 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._isChangingDecorations = false; } - private renderMarkerHover(markerHover: MarkerHover): HTMLElement { - const hoverElement = $('div.hover-row'); - const markerElement = dom.append(hoverElement, $('div.marker.hover-contents')); - const { source, message, code, relatedInformation } = markerHover.marker; - - this._editor.applyFontInfo(markerElement); - const messageElement = dom.append(markerElement, $('span')); - messageElement.style.whiteSpace = 'pre-wrap'; - messageElement.innerText = message; - - if (source || code) { - // Code has link - if (code && typeof code !== 'string') { - const sourceAndCodeElement = $('span'); - if (source) { - const sourceElement = dom.append(sourceAndCodeElement, $('span')); - sourceElement.innerText = source; - } - this._codeLink = dom.append(sourceAndCodeElement, $('a.code-link')); - this._codeLink.setAttribute('href', code.target.toString()); - - this._codeLink.onclick = (e) => { - this._openerService.open(code.target); - e.preventDefault(); - e.stopPropagation(); - }; - - const codeElement = dom.append(this._codeLink, $('span')); - codeElement.innerText = code.value; - - const detailsElement = dom.append(markerElement, sourceAndCodeElement); - detailsElement.style.opacity = '0.6'; - detailsElement.style.paddingLeft = '6px'; - } else { - const detailsElement = dom.append(markerElement, $('span')); - detailsElement.style.opacity = '0.6'; - detailsElement.style.paddingLeft = '6px'; - detailsElement.innerText = source && code ? `${source}(${code})` : source ? source : `(${code})`; - } - } - - if (isNonEmptyArray(relatedInformation)) { - for (const { message, resource, startLineNumber, startColumn } of relatedInformation) { - const relatedInfoContainer = dom.append(markerElement, $('div')); - relatedInfoContainer.style.marginTop = '8px'; - const a = dom.append(relatedInfoContainer, $('a')); - a.innerText = `${basename(resource)}(${startLineNumber}, ${startColumn}): `; - a.style.cursor = 'pointer'; - a.onclick = e => { - e.stopPropagation(); - e.preventDefault(); - if (this._openerService) { - this._openerService.open(resource.with({ fragment: `${startLineNumber},${startColumn}` }), { fromUserGesture: true }).catch(onUnexpectedError); - } - }; - const messageElement = dom.append(relatedInfoContainer, $('span')); - messageElement.innerText = message; - this._editor.applyFontInfo(messageElement); - } - } - - return hoverElement; - } - - private recentMarkerCodeActionsInfo: { marker: IMarker, hasCodeActions: boolean } | undefined = undefined; - private renderMarkerStatusbar(markerHover: MarkerHover): HTMLElement { - const hoverElement = $('div.hover-row.status-bar'); - const disposables = new DisposableStore(); - const actionsElement = dom.append(hoverElement, $('div.actions')); - if (markerHover.marker.severity === MarkerSeverity.Error || markerHover.marker.severity === MarkerSeverity.Warning || markerHover.marker.severity === MarkerSeverity.Info) { - disposables.add(this._renderAction(actionsElement, { - label: nls.localize('peek problem', "Peek Problem"), - commandId: NextMarkerAction.ID, - run: () => { - this.hide(); - MarkerController.get(this._editor).showAtMarker(markerHover.marker); - this._editor.focus(); - } - })); - } - - if (!this._editor.getOption(EditorOption.readOnly)) { - const quickfixPlaceholderElement = dom.append(actionsElement, $('div')); - if (this.recentMarkerCodeActionsInfo) { - if (IMarkerData.makeKey(this.recentMarkerCodeActionsInfo.marker) === IMarkerData.makeKey(markerHover.marker)) { - if (!this.recentMarkerCodeActionsInfo.hasCodeActions) { - quickfixPlaceholderElement.textContent = nls.localize('noQuickFixes', "No quick fixes available"); - } - } else { - this.recentMarkerCodeActionsInfo = undefined; - } - } - const updatePlaceholderDisposable = disposables.add(disposableTimeout(() => quickfixPlaceholderElement.textContent = nls.localize('checkingForQuickFixes', "Checking for quick fixes..."), 64)); - const codeActionsPromise = this.getCodeActions(markerHover.marker); - disposables.add(toDisposable(() => codeActionsPromise.cancel())); - codeActionsPromise.then(actions => { - updatePlaceholderDisposable.dispose(); - this.recentMarkerCodeActionsInfo = { marker: markerHover.marker, hasCodeActions: actions.validActions.length > 0 }; - - if (!this.recentMarkerCodeActionsInfo.hasCodeActions) { - actions.dispose(); - quickfixPlaceholderElement.textContent = nls.localize('noQuickFixes', "No quick fixes available"); - return; - } - quickfixPlaceholderElement.style.display = 'none'; - - let showing = false; - disposables.add(toDisposable(() => { - if (!showing) { - actions.dispose(); - } - })); - - disposables.add(this._renderAction(actionsElement, { - label: nls.localize('quick fixes', "Quick Fix..."), - commandId: QuickFixAction.Id, - run: (target) => { - showing = true; - const controller = QuickFixController.get(this._editor); - const elementPosition = dom.getDomNodePagePosition(target); - // Hide the hover pre-emptively, otherwise the editor can close the code actions - // context menu as well when using keyboard navigation - this.hide(); - controller.showCodeActions(markerCodeActionTrigger, actions, { - x: elementPosition.left + 6, - y: elementPosition.top + elementPosition.height + 6 - }); - } - })); - }); - } - - this.renderDisposable.value = disposables; - return hoverElement; - } - - private getCodeActions(marker: IMarker): CancelablePromise { - return createCancelablePromise(cancellationToken => { - return getCodeActions( - this._editor.getModel()!, - new Range(marker.startLineNumber, marker.startColumn, marker.endLineNumber, marker.endColumn), - markerCodeActionTrigger, - Progress.None, - cancellationToken); - }); - } - private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({ className: 'hoverHighlight' }); } -function hoverContentsEquals(first: HoverPart[], second: HoverPart[]): boolean { - if ((!first && second) || (first && !second) || first.length !== second.length) { +function hoverContentsEquals(first: HoverPartInfo[], second: HoverPartInfo[]): boolean { + if (first.length !== second.length) { return false; } for (let i = 0; i < first.length; i++) { - const firstElement = first[i]; - const secondElement = second[i]; - if (firstElement instanceof MarkerHover && secondElement instanceof MarkerHover) { - return IMarkerData.makeKey(firstElement.marker) === IMarkerData.makeKey(secondElement.marker); - } - if (firstElement instanceof ColorHover || secondElement instanceof ColorHover) { - return false; - } - if (firstElement instanceof MarkerHover || secondElement instanceof MarkerHover) { - return false; - } - if (!markedStringsEquals(firstElement.contents, secondElement.contents)) { + if (!first[i].data.equals(second[i].data)) { return false; } } diff --git a/src/vs/editor/contrib/inlineHints/inlineHintsController.ts b/src/vs/editor/contrib/inlineHints/inlineHintsController.ts new file mode 100644 index 00000000..43750f98 --- /dev/null +++ b/src/vs/editor/contrib/inlineHints/inlineHintsController.ts @@ -0,0 +1,227 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RunOnceScheduler } from 'vs/base/common/async'; +import { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { hash } from 'vs/base/common/hash'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { IContentDecorationRenderOptions, IEditorContribution } from 'vs/editor/common/editorCommon'; +import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; +import { InlineHintsProvider, InlineHintsProviderRegistry, InlineHint } from 'vs/editor/common/modes'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { flatten } from 'vs/base/common/arrays'; +import { editorInlineHintForeground, editorInlineHintBackground } from 'vs/platform/theme/common/colorRegistry'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { Range } from 'vs/editor/common/core/range'; +import { LanguageFeatureRequestDelays } from 'vs/editor/common/modes/languageFeatureRegistry'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { URI } from 'vs/base/common/uri'; +import { IRange } from 'vs/base/common/range'; +import { assertType } from 'vs/base/common/types'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; + +const MAX_DECORATORS = 500; + +export interface InlineHintsData { + list: InlineHint[]; + provider: InlineHintsProvider; +} + +export async function getInlineHints(model: ITextModel, ranges: Range[], token: CancellationToken): Promise { + const datas: InlineHintsData[] = []; + const providers = InlineHintsProviderRegistry.ordered(model).reverse(); + const promises = flatten(providers.map(provider => ranges.map(range => Promise.resolve(provider.provideInlineHints(model, range, token)).then(result => { + if (result) { + datas.push({ list: result, provider }); + } + }, err => { + onUnexpectedExternalError(err); + })))); + + await Promise.all(promises); + + return datas; +} + +export class InlineHintsController implements IEditorContribution { + + static readonly ID: string = 'editor.contrib.InlineHints'; + + // static get(editor: ICodeEditor): InlineHintsController { + // return editor.getContribution(this.ID); + // } + + private readonly _disposables = new DisposableStore(); + private readonly _sessionDisposables = new DisposableStore(); + private readonly _getInlineHintsDelays = new LanguageFeatureRequestDelays(InlineHintsProviderRegistry, 250, 2500); + + private _decorationsTypeIds: string[] = []; + private _decorationIds: string[] = []; + + constructor( + private readonly _editor: ICodeEditor, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, + @IThemeService private readonly _themeService: IThemeService, + ) { + this._disposables.add(InlineHintsProviderRegistry.onDidChange(() => this._update())); + this._disposables.add(_themeService.onDidColorThemeChange(() => this._update())); + this._disposables.add(_editor.onDidChangeModel(() => this._update())); + this._disposables.add(_editor.onDidChangeModelLanguage(() => this._update())); + this._disposables.add(_editor.onDidChangeConfiguration(e => { + if (e.hasChanged(EditorOption.inlineHints)) { + this._update(); + } + })); + + this._update(); + } + + dispose(): void { + this._sessionDisposables.dispose(); + this._removeAllDecorations(); + this._disposables.dispose(); + } + + private _update(): void { + this._sessionDisposables.clear(); + + if (!this._editor.getOption(EditorOption.inlineHints).enabled) { + this._removeAllDecorations(); + return; + } + + const model = this._editor.getModel(); + if (!model || !InlineHintsProviderRegistry.has(model)) { + this._removeAllDecorations(); + return; + } + + const scheduler = new RunOnceScheduler(async () => { + const t1 = Date.now(); + + const cts = new CancellationTokenSource(); + this._sessionDisposables.add(toDisposable(() => cts.dispose(true))); + + const visibleRanges = this._editor.getVisibleRangesPlusViewportAboveBelow(); + const result = await getInlineHints(model, visibleRanges, cts.token); + + // update moving average + const newDelay = this._getInlineHintsDelays.update(model, Date.now() - t1); + scheduler.delay = newDelay; + + // render hints + this._updateHintsDecorators(result); + + }, this._getInlineHintsDelays.get(model)); + + this._sessionDisposables.add(scheduler); + + // update inline hints when content or scroll position changes + this._sessionDisposables.add(this._editor.onDidChangeModelContent(() => scheduler.schedule())); + this._disposables.add(this._editor.onDidScrollChange(() => scheduler.schedule())); + scheduler.schedule(); + + // update inline hints when any any provider fires an event + const providerListener = new DisposableStore(); + this._sessionDisposables.add(providerListener); + for (const provider of InlineHintsProviderRegistry.all(model)) { + if (typeof provider.onDidChangeInlineHints === 'function') { + providerListener.add(provider.onDidChangeInlineHints(() => scheduler.schedule())); + } + } + } + + private _updateHintsDecorators(hintsData: InlineHintsData[]): void { + const { fontSize, fontFamily } = this._getLayoutInfo(); + const backgroundColor = this._themeService.getColorTheme().getColor(editorInlineHintBackground); + const fontColor = this._themeService.getColorTheme().getColor(editorInlineHintForeground); + + const newDecorationsTypeIds: string[] = []; + const newDecorationsData: IModelDeltaDecoration[] = []; + + for (const { list: hints } of hintsData) { + + for (let j = 0; j < hints.length && newDecorationsData.length < MAX_DECORATORS; j++) { + const { text, range, description: hoverMessage, whitespaceBefore, whitespaceAfter } = hints[j]; + const marginBefore = whitespaceBefore ? (fontSize / 3) | 0 : 0; + const marginAfter = whitespaceAfter ? (fontSize / 3) | 0 : 0; + + const before: IContentDecorationRenderOptions = { + contentText: text, + backgroundColor: `${backgroundColor}`, + color: `${fontColor}`, + margin: `0px ${marginAfter}px 0px ${marginBefore}px`, + fontSize: `${fontSize}px`, + fontFamily: fontFamily, + padding: `0px ${(fontSize / 4) | 0}px`, + borderRadius: `${(fontSize / 4) | 0}px`, + }; + const key = 'inlineHints-' + hash(before).toString(16); + this._codeEditorService.registerDecorationType(key, { before }, undefined, this._editor); + + // decoration types are ref-counted which means we only need to + // call register und remove equally often + newDecorationsTypeIds.push(key); + + const options = this._codeEditorService.resolveDecorationOptions(key, true); + if (typeof hoverMessage === 'string') { + options.hoverMessage = new MarkdownString().appendText(hoverMessage); + } else if (hoverMessage) { + options.hoverMessage = hoverMessage; + } + + newDecorationsData.push({ + range, + options + }); + } + } + + this._decorationsTypeIds.forEach(this._codeEditorService.removeDecorationType, this._codeEditorService); + this._decorationsTypeIds = newDecorationsTypeIds; + + this._decorationIds = this._editor.deltaDecorations(this._decorationIds, newDecorationsData); + } + + private _getLayoutInfo() { + const options = this._editor.getOption(EditorOption.inlineHints); + const editorFontSize = this._editor.getOption(EditorOption.fontSize); + let fontSize = options.fontSize; + if (!fontSize || fontSize < 5 || fontSize > editorFontSize) { + fontSize = (editorFontSize * .9) | 0; + } + const fontFamily = options.fontFamily; + return { fontSize, fontFamily }; + } + + private _removeAllDecorations(): void { + this._decorationIds = this._editor.deltaDecorations(this._decorationIds, []); + this._decorationsTypeIds.forEach(this._codeEditorService.removeDecorationType, this._codeEditorService); + this._decorationsTypeIds = []; + } +} + +registerEditorContribution(InlineHintsController.ID, InlineHintsController); + +CommandsRegistry.registerCommand('_executeInlineHintProvider', async (accessor, ...args: [URI, IRange]): Promise => { + + const [uri, range] = args; + assertType(URI.isUri(uri)); + assertType(Range.isIRange(range)); + + const ref = await accessor.get(ITextModelService).createModelReference(uri); + try { + const data = await getInlineHints(ref.object.textEditorModel, [Range.lift(range)], CancellationToken.None); + return flatten(data.map(item => item.list)).sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + + } finally { + ref.dispose(); + } +}); diff --git a/src/vs/editor/contrib/linesOperations/linesOperations.ts b/src/vs/editor/contrib/linesOperations/linesOperations.ts index 9c365d50..54fcba6a 100644 --- a/src/vs/editor/contrib/linesOperations/linesOperations.ts +++ b/src/vs/editor/contrib/linesOperations/linesOperations.ts @@ -939,43 +939,39 @@ export class TransposeAction extends EditorAction { export abstract class AbstractCaseAction extends EditorAction { public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { - let selections = editor.getSelections(); + const selections = editor.getSelections(); if (selections === null) { return; } - let model = editor.getModel(); + const model = editor.getModel(); if (model === null) { return; } - let wordSeparators = editor.getOption(EditorOption.wordSeparators); + const wordSeparators = editor.getOption(EditorOption.wordSeparators); + const textEdits: IIdentifiedSingleEditOperation[] = []; - let commands: ICommand[] = []; - - for (let i = 0, len = selections.length; i < len; i++) { - let selection = selections[i]; + for (const selection of selections) { if (selection.isEmpty()) { - let cursor = selection.getStartPosition(); + const cursor = selection.getStartPosition(); const word = editor.getConfiguredWordAtPosition(cursor); if (!word) { continue; } - let wordRange = new Range(cursor.lineNumber, word.startColumn, cursor.lineNumber, word.endColumn); - let text = model.getValueInRange(wordRange); - commands.push(new ReplaceCommandThatPreservesSelection(wordRange, this._modifyText(text, wordSeparators), - new Selection(cursor.lineNumber, cursor.column, cursor.lineNumber, cursor.column))); - + const wordRange = new Range(cursor.lineNumber, word.startColumn, cursor.lineNumber, word.endColumn); + const text = model.getValueInRange(wordRange); + textEdits.push(EditOperation.replace(wordRange, this._modifyText(text, wordSeparators))); } else { - let text = model.getValueInRange(selection); - commands.push(new ReplaceCommandThatPreservesSelection(selection, this._modifyText(text, wordSeparators), selection)); + const text = model.getValueInRange(selection); + textEdits.push(EditOperation.replace(selection, this._modifyText(text, wordSeparators))); } } editor.pushUndoStop(); - editor.executeCommands(this.id, commands); + editor.executeEdits(this.id, textEdits); editor.pushUndoStop(); } @@ -1049,6 +1045,25 @@ export class TitleCaseAction extends AbstractCaseAction { } } +export class SnakeCaseAction extends AbstractCaseAction { + constructor() { + super({ + id: 'editor.action.transformToSnakecase', + label: nls.localize('editor.transformToSnakecase', "Transform to Snake Case"), + alias: 'Transform to Snake Case', + precondition: EditorContextKeys.writable + }); + } + + protected _modifyText(text: string, wordSeparators: string): string { + return (text + .replace(/(\p{Ll})(\p{Lu})/gmu, '$1_$2') + .replace(/([^\b_])(\p{Lu})(\p{Ll})/gmu, '$1_$2$3') + .toLocaleLowerCase() + ); + } +} + registerEditorAction(CopyLinesUpAction); registerEditorAction(CopyLinesDownAction); registerEditorAction(DuplicateSelectionAction); @@ -1069,3 +1084,4 @@ registerEditorAction(TransposeAction); registerEditorAction(UpperCaseAction); registerEditorAction(LowerCaseAction); registerEditorAction(TitleCaseAction); +registerEditorAction(SnakeCaseAction); diff --git a/src/vs/editor/contrib/linesOperations/moveLinesCommand.ts b/src/vs/editor/contrib/linesOperations/moveLinesCommand.ts index 681c8f02..6cdc4a4d 100644 --- a/src/vs/editor/contrib/linesOperations/moveLinesCommand.ts +++ b/src/vs/editor/contrib/linesOperations/moveLinesCommand.ts @@ -299,13 +299,13 @@ export class MoveLinesCommand implements ICommand { } } - private matchEnterRule(model: ITextModel, indentConverter: IIndentConverter, tabSize: number, line: number, oneLineAbove: number, oneLineAboveText?: string) { + private matchEnterRule(model: ITextModel, indentConverter: IIndentConverter, tabSize: number, line: number, oneLineAbove: number, previousLineText?: string) { let validPrecedingLine = oneLineAbove; while (validPrecedingLine >= 1) { // ship empty lines as empty lines just inherit indentation let lineContent; - if (validPrecedingLine === oneLineAbove && oneLineAboveText !== undefined) { - lineContent = oneLineAboveText; + if (validPrecedingLine === oneLineAbove && previousLineText !== undefined) { + lineContent = previousLineText; } else { lineContent = model.getLineContent(validPrecedingLine); } diff --git a/src/vs/editor/contrib/linesOperations/test/copyLinesCommand.test.ts b/src/vs/editor/contrib/linesOperations/test/copyLinesCommand.test.ts index 6777693d..64f3c7a5 100644 --- a/src/vs/editor/contrib/linesOperations/test/copyLinesCommand.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/copyLinesCommand.test.ts @@ -207,8 +207,8 @@ suite('Editor Contrib - Duplicate Selection', () => { withTestCodeEditor(lines.join('\n'), {}, (editor) => { editor.setSelections(selections); duplicateSelectionAction.run(null!, editor, {}); - assert.deepEqual(editor.getValue(), expectedLines.join('\n')); - assert.deepEqual(editor.getSelections()!.map(s => s.toString()), expectedSelections.map(s => s.toString())); + assert.deepStrictEqual(editor.getValue(), expectedLines.join('\n')); + assert.deepStrictEqual(editor.getSelections()!.map(s => s.toString()), expectedSelections.map(s => s.toString())); }); } diff --git a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts index cdcb7f49..b3cbe4bd 100644 --- a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts @@ -8,7 +8,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { Handler } from 'vs/editor/common/editorCommon'; 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 { TitleCaseAction, DeleteAllLeftAction, DeleteAllRightAction, IndentLinesAction, InsertLineAfterAction, InsertLineBeforeAction, JoinLinesAction, LowerCaseAction, SortLinesAscendingAction, SortLinesDescendingAction, TransposeAction, UpperCaseAction, DeleteLinesAction, SnakeCaseAction } 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'; @@ -19,7 +19,7 @@ function assertSelection(editor: ICodeEditor, expected: Selection | Selection[]) if (!Array.isArray(expected)) { expected = [expected]; } - assert.deepEqual(editor.getSelections(), expected); + assert.deepStrictEqual(editor.getSelections(), expected); } function executeAction(action: EditorAction, editor: ICodeEditor): void { @@ -40,7 +40,7 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 1, 3, 5)); executeAction(sortLinesAscendingAction, editor); - assert.deepEqual(model.getLinesContent(), [ + assert.deepStrictEqual(model.getLinesContent(), [ 'alpha', 'beta', 'omicron' @@ -65,7 +65,7 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelections([new Selection(1, 1, 3, 5), new Selection(5, 1, 7, 5)]); executeAction(sortLinesAscendingAction, editor); - assert.deepEqual(model.getLinesContent(), [ + assert.deepStrictEqual(model.getLinesContent(), [ 'alpha', 'beta', 'omicron', @@ -79,7 +79,7 @@ suite('Editor Contrib - Line Operations', () => { new Selection(5, 1, 7, 7) ]; editor.getSelections()!.forEach((actualSelection, index) => { - assert.deepEqual(actualSelection.toString(), expectedSelections[index].toString()); + assert.deepStrictEqual(actualSelection.toString(), expectedSelections[index].toString()); }); }); }); @@ -98,7 +98,7 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 1, 3, 7)); executeAction(sortLinesDescendingAction, editor); - assert.deepEqual(model.getLinesContent(), [ + assert.deepStrictEqual(model.getLinesContent(), [ 'omicron', 'beta', 'alpha' @@ -123,7 +123,7 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelections([new Selection(1, 1, 3, 7), new Selection(5, 1, 7, 7)]); executeAction(sortLinesDescendingAction, editor); - assert.deepEqual(model.getLinesContent(), [ + assert.deepStrictEqual(model.getLinesContent(), [ 'omicron', 'beta', 'alpha', @@ -137,7 +137,7 @@ suite('Editor Contrib - Line Operations', () => { new Selection(5, 1, 7, 5) ]; editor.getSelections()!.forEach((actualSelection, index) => { - assert.deepEqual(actualSelection.toString(), expectedSelections[index].toString()); + assert.deepStrictEqual(actualSelection.toString(), expectedSelections[index].toString()); }); }); }); @@ -157,12 +157,12 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 2, 1, 2)); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(1), 'ne'); + assert.strictEqual(model.getLineContent(1), 'ne'); editor.setSelections([new Selection(2, 2, 2, 2), new Selection(3, 2, 3, 2)]); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(2), 'wo'); - assert.equal(model.getLineContent(3), 'hree'); + assert.strictEqual(model.getLineContent(2), 'wo'); + assert.strictEqual(model.getLineContent(3), 'hree'); }); }); @@ -178,16 +178,16 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(2, 1, 2, 1)); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(1), 'onetwo'); + assert.strictEqual(model.getLineContent(1), 'onetwo'); editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 1, 2, 1)]); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLinesContent()[0], 'onetwothree'); - assert.equal(model.getLinesContent().length, 1); + assert.strictEqual(model.getLinesContent()[0], 'onetwothree'); + assert.strictEqual(model.getLinesContent().length, 1); editor.setSelection(new Selection(1, 1, 1, 1)); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLinesContent()[0], 'onetwothree'); + assert.strictEqual(model.getLinesContent()[0], 'onetwothree'); }); }); @@ -213,25 +213,25 @@ suite('Editor Contrib - Line Operations', () => { executeAction(deleteAllLeftAction, editor); let selections = editor.getSelections()!; - assert.equal(model.getLineContent(2), ''); - assert.equal(model.getLineContent(3), ' waso waso'); - assert.equal(model.getLineContent(5), ''); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), ' waso waso'); + assert.strictEqual(model.getLineContent(5), ''); - assert.deepEqual([ + assert.deepStrictEqual([ selections[0].startLineNumber, selections[0].startColumn, selections[0].endLineNumber, selections[0].endColumn ], [3, 1, 3, 1]); - assert.deepEqual([ + assert.deepStrictEqual([ selections[1].startLineNumber, selections[1].startColumn, selections[1].endLineNumber, selections[1].endColumn ], [2, 1, 2, 1]); - assert.deepEqual([ + assert.deepStrictEqual([ selections[2].startLineNumber, selections[2].startColumn, selections[2].endLineNumber, @@ -241,17 +241,17 @@ suite('Editor Contrib - Line Operations', () => { executeAction(deleteAllLeftAction, editor); selections = editor.getSelections()!; - assert.equal(model.getLineContent(1), 'hi my name is Carlos Matos waso waso'); - assert.equal(selections.length, 2); + assert.strictEqual(model.getLineContent(1), 'hi my name is Carlos Matos waso waso'); + assert.strictEqual(selections.length, 2); - assert.deepEqual([ + assert.deepStrictEqual([ selections[0].startLineNumber, selections[0].startColumn, selections[0].endLineNumber, selections[0].endColumn ], [1, 27, 1, 27]); - assert.deepEqual([ + assert.deepStrictEqual([ selections[1].startLineNumber, selections[1].startColumn, selections[1].endLineNumber, @@ -277,23 +277,23 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelections([new Selection(1, 2, 1, 2), new Selection(1, 4, 1, 4)]); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(1), 'lo'); + assert.strictEqual(model.getLineContent(1), 'lo'); editor.setSelections([new Selection(2, 2, 2, 2), new Selection(2, 4, 2, 5)]); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(2), 'd'); + assert.strictEqual(model.getLineContent(2), 'd'); editor.setSelections([new Selection(3, 2, 3, 5), new Selection(3, 7, 3, 7)]); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(3), 'world'); + assert.strictEqual(model.getLineContent(3), 'world'); editor.setSelections([new Selection(4, 3, 4, 3), new Selection(4, 5, 5, 4)]); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(4), 'jour'); + assert.strictEqual(model.getLineContent(4), 'jour'); editor.setSelections([new Selection(5, 3, 6, 3), new Selection(6, 5, 7, 5), new Selection(7, 7, 7, 7)]); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(5), 'world'); + assert.strictEqual(model.getLineContent(5), 'world'); }); }); @@ -310,16 +310,16 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 1, 1, 1)); editor.trigger('keyboard', Handler.Type, { text: 'Typing some text here on line ' }); - assert.equal(model.getLineContent(1), 'Typing some text here on line one'); - assert.deepEqual(editor.getSelection(), new Selection(1, 31, 1, 31)); + assert.strictEqual(model.getLineContent(1), 'Typing some text here on line one'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 31, 1, 31)); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(1), 'one'); - assert.deepEqual(editor.getSelection(), new Selection(1, 1, 1, 1)); + assert.strictEqual(model.getLineContent(1), 'one'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 1, 1, 1)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Typing some text here on line one'); - assert.deepEqual(editor.getSelection(), new Selection(1, 31, 1, 31)); + assert.strictEqual(model.getLineContent(1), 'Typing some text here on line one'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 31, 1, 31)); }); }); }); @@ -345,27 +345,27 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 2, 1, 2)); executeAction(joinLinesAction, editor); - assert.equal(model.getLineContent(1), 'hello world'); + assert.strictEqual(model.getLineContent(1), 'hello world'); assertSelection(editor, new Selection(1, 6, 1, 6)); editor.setSelection(new Selection(2, 2, 2, 2)); executeAction(joinLinesAction, editor); - assert.equal(model.getLineContent(2), 'hello world'); + assert.strictEqual(model.getLineContent(2), 'hello world'); assertSelection(editor, new Selection(2, 7, 2, 7)); editor.setSelection(new Selection(3, 2, 3, 2)); executeAction(joinLinesAction, editor); - assert.equal(model.getLineContent(3), 'hello world'); + assert.strictEqual(model.getLineContent(3), 'hello world'); assertSelection(editor, new Selection(3, 7, 3, 7)); editor.setSelection(new Selection(4, 2, 5, 3)); executeAction(joinLinesAction, editor); - assert.equal(model.getLineContent(4), 'hello world'); + assert.strictEqual(model.getLineContent(4), 'hello world'); assertSelection(editor, new Selection(4, 2, 4, 8)); editor.setSelection(new Selection(5, 1, 7, 3)); executeAction(joinLinesAction, editor); - assert.equal(model.getLineContent(5), 'hello world'); + assert.strictEqual(model.getLineContent(5), 'hello world'); assertSelection(editor, new Selection(5, 1, 5, 3)); }); }); @@ -381,8 +381,8 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(2, 1, 2, 1)); executeAction(joinLinesAction, editor); - assert.equal(model.getLineContent(1), 'hello'); - assert.equal(model.getLineContent(2), 'world'); + assert.strictEqual(model.getLineContent(1), 'hello'); + assert.strictEqual(model.getLineContent(2), 'world'); assertSelection(editor, new Selection(2, 6, 2, 6)); }); }); @@ -416,7 +416,7 @@ suite('Editor Contrib - Line Operations', () => { ]); executeAction(joinLinesAction, editor); - assert.equal(model.getLinesContent().join('\n'), 'hello world\nhello world\nhello world\nhello world\n\nhello world'); + assert.strictEqual(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), @@ -440,16 +440,16 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 6, 1, 6)); editor.trigger('keyboard', Handler.Type, { text: ' my dear' }); - assert.equal(model.getLineContent(1), 'hello my dear'); - assert.deepEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); + assert.strictEqual(model.getLineContent(1), 'hello my dear'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); executeAction(joinLinesAction, editor); - assert.equal(model.getLineContent(1), 'hello my dear world'); - assert.deepEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); + assert.strictEqual(model.getLineContent(1), 'hello my dear world'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'hello my dear'); - assert.deepEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); + assert.strictEqual(model.getLineContent(1), 'hello my dear'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); }); }); }); @@ -467,27 +467,27 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 1, 1, 1)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(1), 'hello world'); + assert.strictEqual(model.getLineContent(1), 'hello world'); assertSelection(editor, new Selection(1, 2, 1, 2)); editor.setSelection(new Selection(1, 6, 1, 6)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(1), 'hell oworld'); + assert.strictEqual(model.getLineContent(1), 'hell oworld'); assertSelection(editor, new Selection(1, 7, 1, 7)); editor.setSelection(new Selection(1, 12, 1, 12)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(1), 'hell oworl'); + assert.strictEqual(model.getLineContent(1), 'hell oworl'); assertSelection(editor, new Selection(2, 2, 2, 2)); editor.setSelection(new Selection(3, 1, 3, 1)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(3), ''); assertSelection(editor, new Selection(4, 1, 4, 1)); editor.setSelection(new Selection(4, 2, 4, 2)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(4), ' '); + assert.strictEqual(model.getLineContent(4), ' '); assertSelection(editor, new Selection(4, 3, 4, 3)); } ); @@ -509,22 +509,22 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 1, 1, 1)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(2), ''); assertSelection(editor, new Selection(2, 1, 2, 1)); editor.setSelection(new Selection(3, 6, 3, 6)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(4), 'oworld'); + assert.strictEqual(model.getLineContent(4), 'oworld'); assertSelection(editor, new Selection(4, 2, 4, 2)); editor.setSelection(new Selection(6, 12, 6, 12)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(7), 'd'); + assert.strictEqual(model.getLineContent(7), 'd'); assertSelection(editor, new Selection(7, 2, 7, 2)); editor.setSelection(new Selection(8, 12, 8, 12)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(8), 'hello world'); + assert.strictEqual(model.getLineContent(8), 'hello world'); assertSelection(editor, new Selection(8, 12, 8, 12)); } ); @@ -534,52 +534,131 @@ suite('Editor Contrib - Line Operations', () => { withTestCodeEditor( [ 'hello world', - 'öçşğü' + 'öçşğü', + 'parseHTMLString', + 'getElementById', + 'insertHTML', + 'PascalCase', + 'CSSSelectorsList', + 'iD', + 'tEST', + 'öçşÖÇŞğüĞÜ', + 'audioConverter.convertM4AToMP3();', + 'snake_case', + 'Capital_Snake_Case', + `function helloWorld() { + return someGlobalObject.printHelloWorld("en", "utf-8"); + } + helloWorld();`.replace(/^\s+/gm, '') ], {}, (editor) => { let model = editor.getModel()!; let uppercaseAction = new UpperCaseAction(); let lowercaseAction = new LowerCaseAction(); let titlecaseAction = new TitleCaseAction(); + let snakecaseAction = new SnakeCaseAction(); editor.setSelection(new Selection(1, 1, 1, 12)); executeAction(uppercaseAction, editor); - assert.equal(model.getLineContent(1), 'HELLO WORLD'); + assert.strictEqual(model.getLineContent(1), 'HELLO WORLD'); assertSelection(editor, new Selection(1, 1, 1, 12)); editor.setSelection(new Selection(1, 1, 1, 12)); executeAction(lowercaseAction, editor); - assert.equal(model.getLineContent(1), 'hello world'); + assert.strictEqual(model.getLineContent(1), 'hello world'); assertSelection(editor, new Selection(1, 1, 1, 12)); editor.setSelection(new Selection(1, 3, 1, 3)); executeAction(uppercaseAction, editor); - assert.equal(model.getLineContent(1), 'HELLO world'); + assert.strictEqual(model.getLineContent(1), 'HELLO world'); assertSelection(editor, new Selection(1, 3, 1, 3)); editor.setSelection(new Selection(1, 4, 1, 4)); executeAction(lowercaseAction, editor); - assert.equal(model.getLineContent(1), 'hello world'); + assert.strictEqual(model.getLineContent(1), 'hello world'); assertSelection(editor, new Selection(1, 4, 1, 4)); editor.setSelection(new Selection(1, 1, 1, 12)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(1), 'Hello World'); + assert.strictEqual(model.getLineContent(1), 'Hello World'); assertSelection(editor, new Selection(1, 1, 1, 12)); editor.setSelection(new Selection(2, 1, 2, 6)); executeAction(uppercaseAction, editor); - assert.equal(model.getLineContent(2), 'ÖÇŞĞÜ'); + assert.strictEqual(model.getLineContent(2), 'ÖÇŞĞÜ'); assertSelection(editor, new Selection(2, 1, 2, 6)); editor.setSelection(new Selection(2, 1, 2, 6)); executeAction(lowercaseAction, editor); - assert.equal(model.getLineContent(2), 'öçşğü'); + assert.strictEqual(model.getLineContent(2), 'öçşğü'); assertSelection(editor, new Selection(2, 1, 2, 6)); editor.setSelection(new Selection(2, 1, 2, 6)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(2), 'Öçşğü'); + assert.strictEqual(model.getLineContent(2), 'Öçşğü'); assertSelection(editor, new Selection(2, 1, 2, 6)); + + editor.setSelection(new Selection(3, 1, 3, 16)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(3), 'parse_html_string'); + assertSelection(editor, new Selection(3, 1, 3, 18)); + + editor.setSelection(new Selection(4, 1, 4, 15)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(4), 'get_element_by_id'); + assertSelection(editor, new Selection(4, 1, 4, 18)); + + editor.setSelection(new Selection(5, 1, 5, 11)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(5), 'insert_html'); + assertSelection(editor, new Selection(5, 1, 5, 12)); + + editor.setSelection(new Selection(6, 1, 6, 11)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(6), 'pascal_case'); + assertSelection(editor, new Selection(6, 1, 6, 12)); + + editor.setSelection(new Selection(7, 1, 7, 17)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(7), 'css_selectors_list'); + assertSelection(editor, new Selection(7, 1, 7, 19)); + + editor.setSelection(new Selection(8, 1, 8, 3)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(8), 'i_d'); + assertSelection(editor, new Selection(8, 1, 8, 4)); + + editor.setSelection(new Selection(9, 1, 9, 5)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(9), 't_est'); + assertSelection(editor, new Selection(9, 1, 9, 6)); + + editor.setSelection(new Selection(10, 1, 10, 11)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(10), 'öçş_öç_şğü_ğü'); + assertSelection(editor, new Selection(10, 1, 10, 14)); + + editor.setSelection(new Selection(11, 1, 11, 34)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(11), 'audio_converter.convert_m4a_to_mp3();'); + assertSelection(editor, new Selection(11, 1, 11, 38)); + + editor.setSelection(new Selection(12, 1, 12, 11)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(12), 'snake_case'); + assertSelection(editor, new Selection(12, 1, 12, 11)); + + editor.setSelection(new Selection(13, 1, 13, 19)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(13), 'capital_snake_case'); + assertSelection(editor, new Selection(13, 1, 13, 19)); + + editor.setSelection(new Selection(14, 1, 17, 14)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getValueInRange(new Selection(14, 1, 17, 15)), `function hello_world() { + return some_global_object.print_hello_world("en", "utf-8"); + } + hello_world();`.replace(/^\s+/gm, '')); + assertSelection(editor, new Selection(14, 1, 17, 15)); } ); @@ -597,27 +676,27 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 1, 1, 12)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(1), 'Foo Bar Baz'); + assert.strictEqual(model.getLineContent(1), 'Foo Bar Baz'); editor.setSelection(new Selection(2, 1, 2, 12)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(2), 'Foo\'Bar\'Baz'); + assert.strictEqual(model.getLineContent(2), 'Foo\'Bar\'Baz'); editor.setSelection(new Selection(3, 1, 3, 12)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(3), 'Foo[Bar]Baz'); + assert.strictEqual(model.getLineContent(3), 'Foo[Bar]Baz'); editor.setSelection(new Selection(4, 1, 4, 12)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(4), 'Foo`Bar~Baz'); + assert.strictEqual(model.getLineContent(4), 'Foo`Bar~Baz'); editor.setSelection(new Selection(5, 1, 5, 12)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(5), 'Foo^Bar%Baz'); + assert.strictEqual(model.getLineContent(5), 'Foo^Bar%Baz'); editor.setSelection(new Selection(6, 1, 6, 12)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(6), 'Foo$Bar!Baz'); + assert.strictEqual(model.getLineContent(6), 'Foo$Bar!Baz'); } ); @@ -632,22 +711,22 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 1, 1, 1)); executeAction(uppercaseAction, editor); - assert.equal(model.getLineContent(1), ''); + assert.strictEqual(model.getLineContent(1), ''); assertSelection(editor, new Selection(1, 1, 1, 1)); editor.setSelection(new Selection(1, 1, 1, 1)); executeAction(lowercaseAction, editor); - assert.equal(model.getLineContent(1), ''); + assert.strictEqual(model.getLineContent(1), ''); assertSelection(editor, new Selection(1, 1, 1, 1)); editor.setSelection(new Selection(2, 2, 2, 2)); executeAction(uppercaseAction, editor); - assert.equal(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(2), ' '); assertSelection(editor, new Selection(2, 2, 2, 2)); editor.setSelection(new Selection(2, 2, 2, 2)); executeAction(lowercaseAction, editor); - assert.equal(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(2), ' '); assertSelection(editor, new Selection(2, 2, 2, 2)); } ); @@ -660,18 +739,18 @@ suite('Editor Contrib - Line Operations', () => { const action = new DeleteAllRightAction(); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); + assert.deepStrictEqual(model.getLinesContent(), ['']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); editor.setSelection(new Selection(1, 1, 1, 1)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); + assert.deepStrictEqual(model.getLinesContent(), ['']); + assert.deepStrictEqual(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)]); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); + assert.deepStrictEqual(model.getLinesContent(), ['']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); }); }); @@ -685,18 +764,18 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 2, 1, 5)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['ho', 'world']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 2, 1, 2)]); + assert.deepStrictEqual(model.getLinesContent(), ['ho', 'world']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 2, 1, 2)]); editor.setSelection(new Selection(1, 1, 2, 4)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['ld']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); + assert.deepStrictEqual(model.getLinesContent(), ['ld']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); editor.setSelection(new Selection(1, 1, 1, 3)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); + assert.deepStrictEqual(model.getLinesContent(), ['']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); }); }); @@ -710,13 +789,13 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 3, 1, 3)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['he', 'world']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 3, 1, 3)]); + assert.deepStrictEqual(model.getLinesContent(), ['he', 'world']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 3, 1, 3)]); editor.setSelection(new Selection(2, 1, 2, 1)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['he', '']); - assert.deepEqual(editor.getSelections(), [new Selection(2, 1, 2, 1)]); + assert.deepStrictEqual(model.getLinesContent(), ['he', '']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(2, 1, 2, 1)]); }); }); @@ -730,18 +809,18 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 6, 1, 6)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['helloworld']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); + assert.deepStrictEqual(model.getLinesContent(), ['helloworld']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); editor.setSelection(new Selection(1, 6, 1, 6)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['hello']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); + assert.deepStrictEqual(model.getLinesContent(), ['hello']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); editor.setSelection(new Selection(1, 6, 1, 6)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['hello']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); + assert.deepStrictEqual(model.getLinesContent(), ['hello']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); }); }); @@ -760,35 +839,35 @@ suite('Editor Contrib - Line Operations', () => { new Selection(3, 4, 3, 4), ]); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['hethere', 'wor']); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(model.getLinesContent(), ['hethere', 'wor']); + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(2, 4, 2, 4) ]); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['he', 'wor']); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(model.getLinesContent(), ['he', 'wor']); + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(2, 4, 2, 4) ]); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['hewor']); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(model.getLinesContent(), ['hewor']); + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(1, 6, 1, 6) ]); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['he']); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(model.getLinesContent(), ['he']); + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3) ]); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['he']); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(model.getLinesContent(), ['he']); + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3) ]); }); @@ -809,20 +888,20 @@ suite('Editor Contrib - Line Operations', () => { new Selection(3, 4, 3, 4), ]); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['hethere', 'wor']); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(model.getLinesContent(), ['hethere', 'wor']); + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(2, 4, 2, 4) ]); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(1, 6, 1, 6), new Selection(3, 4, 3, 4) ]); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(2, 4, 2, 4) ]); @@ -847,27 +926,27 @@ suite('Editor Contrib - Line Operations', () => { } testInsertLineBefore(1, 3, (model, viewModel) => { - assert.deepEqual(viewModel.getSelection(), new Selection(1, 1, 1, 1)); - assert.equal(model.getLineContent(1), ''); - assert.equal(model.getLineContent(2), 'First line'); - assert.equal(model.getLineContent(3), 'Second line'); - assert.equal(model.getLineContent(4), 'Third line'); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(1, 1, 1, 1)); + assert.strictEqual(model.getLineContent(1), ''); + assert.strictEqual(model.getLineContent(2), 'First line'); + assert.strictEqual(model.getLineContent(3), 'Second line'); + assert.strictEqual(model.getLineContent(4), 'Third line'); }); testInsertLineBefore(2, 3, (model, viewModel) => { - assert.deepEqual(viewModel.getSelection(), new Selection(2, 1, 2, 1)); - assert.equal(model.getLineContent(1), 'First line'); - assert.equal(model.getLineContent(2), ''); - assert.equal(model.getLineContent(3), 'Second line'); - assert.equal(model.getLineContent(4), 'Third line'); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(2, 1, 2, 1)); + assert.strictEqual(model.getLineContent(1), 'First line'); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), 'Second line'); + assert.strictEqual(model.getLineContent(4), 'Third line'); }); testInsertLineBefore(3, 3, (model, viewModel) => { - assert.deepEqual(viewModel.getSelection(), new Selection(3, 1, 3, 1)); - assert.equal(model.getLineContent(1), 'First line'); - assert.equal(model.getLineContent(2), 'Second line'); - assert.equal(model.getLineContent(3), ''); - assert.equal(model.getLineContent(4), 'Third line'); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(3, 1, 3, 1)); + assert.strictEqual(model.getLineContent(1), 'First line'); + assert.strictEqual(model.getLineContent(2), 'Second line'); + assert.strictEqual(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(4), 'Third line'); }); }); @@ -888,27 +967,27 @@ suite('Editor Contrib - Line Operations', () => { } testInsertLineAfter(1, 3, (model, viewModel) => { - assert.deepEqual(viewModel.getSelection(), new Selection(2, 1, 2, 1)); - assert.equal(model.getLineContent(1), 'First line'); - assert.equal(model.getLineContent(2), ''); - assert.equal(model.getLineContent(3), 'Second line'); - assert.equal(model.getLineContent(4), 'Third line'); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(2, 1, 2, 1)); + assert.strictEqual(model.getLineContent(1), 'First line'); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), 'Second line'); + assert.strictEqual(model.getLineContent(4), 'Third line'); }); testInsertLineAfter(2, 3, (model, viewModel) => { - assert.deepEqual(viewModel.getSelection(), new Selection(3, 1, 3, 1)); - assert.equal(model.getLineContent(1), 'First line'); - assert.equal(model.getLineContent(2), 'Second line'); - assert.equal(model.getLineContent(3), ''); - assert.equal(model.getLineContent(4), 'Third line'); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(3, 1, 3, 1)); + assert.strictEqual(model.getLineContent(1), 'First line'); + assert.strictEqual(model.getLineContent(2), 'Second line'); + assert.strictEqual(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(4), 'Third line'); }); testInsertLineAfter(3, 3, (model, viewModel) => { - assert.deepEqual(viewModel.getSelection(), new Selection(4, 1, 4, 1)); - assert.equal(model.getLineContent(1), 'First line'); - assert.equal(model.getLineContent(2), 'Second line'); - assert.equal(model.getLineContent(3), 'Third line'); - assert.equal(model.getLineContent(4), ''); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(4, 1, 4, 1)); + assert.strictEqual(model.getLineContent(1), 'First line'); + assert.strictEqual(model.getLineContent(2), 'Second line'); + assert.strictEqual(model.getLineContent(3), 'Third line'); + assert.strictEqual(model.getLineContent(4), ''); }); }); @@ -928,11 +1007,11 @@ suite('Editor Contrib - Line Operations', () => { editor.setPosition(new Position(1, 2)); executeAction(indentLinesAction, editor); - assert.equal(model.getLineContent(1), '\tfunction baz() {'); - assert.deepEqual(editor.getSelection(), new Selection(1, 3, 1, 3)); + assert.strictEqual(model.getLineContent(1), '\tfunction baz() {'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 3, 1, 3)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), '\tf\tunction baz() {'); + assert.strictEqual(model.getLineContent(1), '\tf\tunction baz() {'); }); model.dispose(); @@ -953,8 +1032,8 @@ suite('Editor Contrib - Line Operations', () => { editor.setPosition(new Position(1, 1)); executeAction(indentLinesAction, editor); - assert.equal(model.getLineContent(1), '\tSome text'); - assert.deepEqual(editor.getSelection(), new Selection(1, 2, 1, 2)); + assert.strictEqual(model.getLineContent(1), '\tSome text'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 2, 1, 2)); }); model.dispose(); @@ -972,8 +1051,8 @@ suite('Editor Contrib - Line Operations', () => { editor.setPosition(new Position(1, 1)); executeAction(indentLinesAction, editor); - assert.equal(model.getLineContent(1), ' '); - assert.deepEqual(editor.getSelection(), new Selection(1, 5, 1, 5)); + assert.strictEqual(model.getLineContent(1), ' '); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 5, 1, 5)); }); model.dispose(); @@ -995,7 +1074,7 @@ suite('Editor Contrib - Line Operations', () => { const deleteLinesAction = new DeleteLinesAction(); executeAction(deleteLinesAction, editor); - assert.equal(editor.getValue(), 'a\nc'); + assert.strictEqual(editor.getValue(), 'a\nc'); }); }); @@ -1007,8 +1086,8 @@ suite('Editor Contrib - Line Operations', () => { const deleteLinesAction = new DeleteLinesAction(); executeAction(deleteLinesAction, editor); - assert.equal(editor.getValue(), resultingText.join('\n')); - assert.deepEqual(editor.getSelections(), resultingSelections); + assert.strictEqual(editor.getValue(), resultingText.join('\n')); + assert.deepStrictEqual(editor.getSelections(), resultingSelections); }); } diff --git a/src/vs/editor/contrib/links/links.ts b/src/vs/editor/contrib/links/links.ts index 04e8b8aa..8997154c 100644 --- a/src/vs/editor/contrib/links/links.ts +++ b/src/vs/editor/contrib/links/links.ts @@ -57,7 +57,7 @@ function getHoverMessage(link: Link, useMetaKey: boolean): MarkdownString { nativeLabel = ` "${nativeLabelText}"`; } } - const hoverMessage = new MarkdownString('', true).appendMarkdown(`[${label}](${link.url.toString()}${nativeLabel}) (${kb})`); + const hoverMessage = new MarkdownString('', true).appendMarkdown(`[${label}](${link.url.toString(true)}${nativeLabel}) (${kb})`); return hoverMessage; } else { return new MarkdownString().appendText(`${label} (${kb})`); @@ -327,7 +327,7 @@ export class LinkDetector implements IEditorContribution { } } - return this.openerService.open(uri, { openToSide, fromUserGesture }); + return this.openerService.open(uri, { openToSide, fromUserGesture, allowContributedOpeners: true }); }, err => { const messageOrError = diff --git a/src/vs/editor/contrib/multicursor/multicursor.ts b/src/vs/editor/contrib/multicursor/multicursor.ts index 3606348f..59994885 100644 --- a/src/vs/editor/contrib/multicursor/multicursor.ts +++ b/src/vs/editor/contrib/multicursor/multicursor.ts @@ -850,7 +850,6 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut this.updateSoon.schedule(); } else { this._setState(null); - } } else { this._update(); @@ -1016,11 +1015,6 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut }); this.decorations = this.editor.deltaDecorations(this.decorations, decorations); - - const currentFindState = CommonFindController.get(this.editor).getState(); - if (currentFindState.isRegex || currentFindState.matchCase || currentFindState.wholeWord) { - CommonFindController.get(this.editor).highlightFindOptions(true); - } } private static readonly _SELECTION_HIGHLIGHT_OVERVIEW = ModelDecorationOptions.register({ diff --git a/src/vs/editor/contrib/multicursor/test/multicursor.test.ts b/src/vs/editor/contrib/multicursor/test/multicursor.test.ts index 6e1e0a9b..5a0e7e19 100644 --- a/src/vs/editor/contrib/multicursor/test/multicursor.test.ts +++ b/src/vs/editor/contrib/multicursor/test/multicursor.test.ts @@ -25,7 +25,7 @@ suite('Multicursor', () => { editor.setSelection(new Selection(2, 1, 2, 1)); addCursorUpAction.run(null!, editor, {}); - assert.equal(viewModel.getSelections().length, 2); + assert.strictEqual(viewModel.getSelections().length, 2); editor.trigger('test', Handler.Paste, { text: '1\n2', @@ -35,8 +35,8 @@ suite('Multicursor', () => { ] }); - assert.equal(editor.getModel()!.getLineContent(1), '1abc'); - assert.equal(editor.getModel()!.getLineContent(2), '2def'); + assert.strictEqual(editor.getModel()!.getLineContent(1), '1abc'); + assert.strictEqual(editor.getModel()!.getLineContent(2), '2def'); }); }); @@ -46,7 +46,7 @@ suite('Multicursor', () => { ], {}, (editor, viewModel) => { let addCursorDownAction = new InsertCursorBelow(); addCursorDownAction.run(null!, editor, {}); - assert.equal(viewModel.getSelections().length, 1); + assert.strictEqual(viewModel.getSelections().length, 1); }); }); @@ -90,7 +90,7 @@ suite('Multicursor selection', () => { editor.setSelection(new Selection(2, 9, 2, 16)); selectHighlightsAction.run(null!, editor); - assert.deepEqual(editor.getSelections()!.map(fromRange), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromRange), [ [2, 9, 2, 16], [1, 9, 1, 16], [3, 9, 3, 16], @@ -98,7 +98,7 @@ suite('Multicursor selection', () => { editor.trigger('test', 'removeSecondaryCursors', null); - assert.deepEqual(fromRange(editor.getSelection()!), [2, 9, 2, 16]); + assert.deepStrictEqual(fromRange(editor.getSelection()!), [2, 9, 2, 16]); multiCursorSelectController.dispose(); findController.dispose(); @@ -121,13 +121,13 @@ suite('Multicursor selection', () => { findController.getState().change({ searchString: 'some+thing', isRegex: true, isRevealed: true }, false); selectHighlightsAction.run(null!, editor); - assert.deepEqual(editor.getSelections()!.map(fromRange), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromRange), [ [1, 1, 1, 10], [2, 1, 2, 11], [3, 1, 3, 12], ]); - assert.equal(findController.getState().searchString, 'some+thing'); + assert.strictEqual(findController.getState().searchString, 'some+thing'); multiCursorSelectController.dispose(); findController.dispose(); @@ -154,14 +154,14 @@ suite('Multicursor selection', () => { editor.setSelection(new Selection(2, 1, 3, 4)); addSelectionToNextFindMatch.run(null!, editor); - assert.deepEqual(editor.getSelections()!.map(fromRange), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromRange), [ [2, 1, 3, 4], [8, 1, 9, 4] ]); editor.trigger('test', 'removeSecondaryCursors', null); - assert.deepEqual(fromRange(editor.getSelection()!), [2, 1, 3, 4]); + assert.deepStrictEqual(fromRange(editor.getSelection()!), [2, 1, 3, 4]); multiCursorSelectController.dispose(); findController.dispose(); @@ -182,7 +182,7 @@ suite('Multicursor selection', () => { editor.setSelection(new Selection(1, 1, 1, 4)); addSelectionToNextFindMatch.run(null!, editor); - assert.deepEqual(editor.getSelections()!.map(fromRange), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromRange), [ [1, 1, 1, 4], [1, 4, 1, 7] ]); @@ -190,7 +190,7 @@ suite('Multicursor selection', () => { addSelectionToNextFindMatch.run(null!, editor); addSelectionToNextFindMatch.run(null!, editor); addSelectionToNextFindMatch.run(null!, editor); - assert.deepEqual(editor.getSelections()!.map(fromRange), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromRange), [ [1, 1, 1, 4], [1, 4, 1, 7], [2, 1, 2, 4], @@ -199,14 +199,14 @@ suite('Multicursor selection', () => { ]); editor.trigger('test', Handler.Type, { text: 'z' }); - assert.deepEqual(editor.getSelections()!.map(fromRange), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromRange), [ [1, 2, 1, 2], [1, 3, 1, 3], [2, 2, 2, 2], [3, 2, 3, 2], [3, 3, 3, 3] ]); - assert.equal(editor.getValue(), [ + assert.strictEqual(editor.getValue(), [ 'zz', 'z', 'zz', @@ -239,14 +239,14 @@ suite('Multicursor selection', () => { editor.setSelection(new Selection(2, 1, 3, 4)); addSelectionToNextFindMatch.run(null!, editor); - assert.deepEqual(editor.getSelections()!.map(fromRange), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromRange), [ [2, 1, 3, 4], [8, 1, 9, 4] ]); editor.trigger('test', 'removeSecondaryCursors', null); - assert.deepEqual(fromRange(editor.getSelection()!), [2, 1, 3, 4]); + assert.deepStrictEqual(fromRange(editor.getSelection()!), [2, 1, 3, 4]); multiCursorSelectController.dispose(); findController.dispose(); @@ -284,25 +284,25 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), @@ -323,20 +323,20 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), @@ -357,20 +357,20 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), @@ -392,14 +392,14 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), @@ -421,14 +421,14 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 5, 1, 10), new Selection(2, 5, 2, 10), new Selection(3, 5, 3, 8), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 5, 1, 10), new Selection(2, 5, 2, 10), new Selection(3, 5, 3, 8), @@ -450,20 +450,20 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(2, 1, 2, 5), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(2, 1, 2, 5), new Selection(3, 1, 3, 5), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(2, 1, 2, 5), new Selection(3, 1, 3, 5), @@ -471,7 +471,7 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(2, 1, 2, 5), new Selection(3, 1, 3, 5), @@ -480,7 +480,7 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(2, 1, 2, 5), new Selection(3, 1, 3, 5), @@ -508,18 +508,18 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(4, 1, 4, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(4, 1, 4, 4), new Selection(6, 2, 6, 5), @@ -534,12 +534,12 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(4, 1, 4, 4), ]); @@ -550,7 +550,7 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), ]); @@ -565,14 +565,14 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(4, 1, 4, 4), new Selection(6, 2, 6, 5), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(4, 1, 4, 4), new Selection(6, 2, 6, 5), diff --git a/src/vs/editor/contrib/parameterHints/parameterHints.css b/src/vs/editor/contrib/parameterHints/parameterHints.css index 03c4e264..64f277cc 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHints.css +++ b/src/vs/editor/contrib/parameterHints/parameterHints.css @@ -10,7 +10,7 @@ line-height: 1.5em; } -.monaco-editor .parameter-hints-widget > .wrapper { +.monaco-editor .parameter-hints-widget > .phwrapper { max-width: 440px; display: flex; flex-direction: row; diff --git a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts index 54b7a807..2f41f489 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts @@ -14,7 +14,7 @@ import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentW import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import * as modes from 'vs/editor/common/modes'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; import { Context } from 'vs/editor/contrib/parameterHints/provideSignatureHelp'; import * as nls from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -27,6 +27,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { assertIsDefined } from 'vs/base/common/types'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; const $ = dom.$; @@ -81,7 +82,7 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { private createParamaterHintDOMNodes() { const element = $('.editor-widget.parameter-hints-widget'); - const wrapper = dom.append(element, $('.wrapper')); + const wrapper = dom.append(element, $('.phwrapper')); wrapper.tabIndex = -1; const controls = dom.append(wrapper, $('.controls')); @@ -225,8 +226,7 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { if (typeof activeParameter.documentation === 'string') { documentation.textContent = activeParameter.documentation; } else { - const renderedContents = this.renderDisposeables.add(this.markdownRenderer.render(activeParameter.documentation)); - renderedContents.element.classList.add('markdown-docs'); + const renderedContents = this.renderMarkdownDocs(activeParameter.documentation); documentation.appendChild(renderedContents.element); } dom.append(this.domNodes.docs, $('p', {}, documentation)); @@ -237,8 +237,7 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { } else if (typeof signature.documentation === 'string') { dom.append(this.domNodes.docs, $('p', {}, signature.documentation)); } else { - const renderedContents = this.renderDisposeables.add(this.markdownRenderer.render(signature.documentation)); - renderedContents.element.classList.add('markdown-docs'); + const renderedContents = this.renderMarkdownDocs(signature.documentation); dom.append(this.domNodes.docs, renderedContents.element); } @@ -265,6 +264,16 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { this.domNodes.scrollbar.scanDomNode(); } + private renderMarkdownDocs(markdown: IMarkdownString | undefined): IMarkdownRenderResult { + const renderedContents = this.renderDisposeables.add(this.markdownRenderer.render(markdown, { + asyncRenderCallback: () => { + this.domNodes?.scrollbar.scanDomNode(); + } + })); + renderedContents.element.classList.add('markdown-docs'); + return renderedContents; + } + private hasDocs(signature: modes.SignatureInformation, activeParameter: modes.ParameterInformation | undefined): boolean { if (activeParameter && typeof activeParameter.documentation === 'string' && assertIsDefined(activeParameter.documentation).length > 0) { return true; @@ -360,7 +369,7 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { const height = Math.max(this.editor.getLayoutInfo().height / 4, 250); const maxHeight = `${height}px`; this.domNodes.element.style.maxHeight = maxHeight; - const wrapper = this.domNodes.element.getElementsByClassName('wrapper') as HTMLCollectionOf; + const wrapper = this.domNodes.element.getElementsByClassName('phwrapper') as HTMLCollectionOf; if (wrapper.length) { wrapper[0].style.maxHeight = maxHeight; } diff --git a/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts b/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts index d513ef4b..6cfa1e42 100644 --- a/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts +++ b/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { first } from 'vs/base/common/async'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { ITextModel } from 'vs/editor/common/model'; @@ -20,19 +19,26 @@ export const Context = { MultipleSignatures: new RawContextKey('parameterHintsMultipleSignatures', false), }; -export function provideSignatureHelp( +export async function provideSignatureHelp( model: ITextModel, position: Position, context: modes.SignatureHelpContext, token: CancellationToken -): Promise { +): Promise { const supports = modes.SignatureHelpProviderRegistry.ordered(model); - return first(supports.map(support => () => { - return Promise.resolve(support.provideSignatureHelp(model, position, token, context)) - .catch(e => onUnexpectedExternalError(e)); - })); + for (const support of supports) { + try { + const result = await support.provideSignatureHelp(model, position, token, context); + if (result) { + return result; + } + } catch (err) { + onUnexpectedExternalError(err); + } + } + return undefined; } CommandsRegistry.registerCommand('_executeSignatureHelpProvider', async (accessor, ...args: [URI, IPosition, string?]) => { diff --git a/src/vs/editor/contrib/peekView/peekView.ts b/src/vs/editor/contrib/peekView/peekView.ts index f265a4bf..36db5b2b 100644 --- a/src/vs/editor/contrib/peekView/peekView.ts +++ b/src/vs/editor/contrib/peekView/peekView.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/peekViewWidget'; import * as dom from 'vs/base/browser/dom'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; -import { ActionBar, IActionBarOptions } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionBar, ActionsOrientation, IActionBarOptions } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action } from 'vs/base/common/actions'; import { Color } from 'vs/base/common/color'; import { Emitter } from 'vs/base/common/event'; @@ -25,8 +25,7 @@ import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { registerColor, contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { Codicon } from 'vs/base/common/codicons'; -import { MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; -import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; export const IPeekViewService = createDecorator('IPeekViewService'); export interface IPeekViewService { @@ -107,6 +106,7 @@ export abstract class PeekViewWidget extends ZoneWidget { private readonly _onDidClose = new Emitter(); readonly onDidClose = this._onDidClose.event; + private disposed?: true; protected _headElement?: HTMLDivElement; protected _primaryHeading?: HTMLElement; @@ -125,8 +125,11 @@ export abstract class PeekViewWidget extends ZoneWidget { } dispose(): void { - super.dispose(); - this._onDidClose.fire(this); + if (!this.disposed) { + this.disposed = true; // prevent consumers who dispose on onDidClose from looping + super.dispose(); + this._onDidClose.fire(this); + } } style(styles: IPeekViewStyles): void { @@ -204,15 +207,8 @@ export abstract class PeekViewWidget extends ZoneWidget { protected _getActionBarOptions(): IActionBarOptions { return { - actionViewItemProvider: action => { - if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action); - } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); - } - - return undefined; - } + actionViewItemProvider: createActionViewItem.bind(undefined, this.instantiationService), + orientation: ActionsOrientation.HORIZONTAL }; } diff --git a/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts b/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts index 7db794fa..ec7d2ab9 100644 --- a/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts @@ -10,7 +10,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { stripCodicons } from 'vs/base/common/codicons'; +import { stripIcons } from 'vs/base/common/iconLabels'; export abstract class AbstractEditorCommandsQuickAccessProvider extends AbstractCommandsQuickAccessProvider { @@ -41,7 +41,7 @@ export abstract class AbstractEditorCommandsQuickAccessProvider extends Abstract editorCommandPicks.push({ commandId: editorAction.id, commandAlias: editorAction.alias, - label: stripCodicons(editorAction.label) || editorAction.id, + label: stripIcons(editorAction.label) || editorAction.id, }); } diff --git a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts index 9fb3e3d3..96c0a9be 100644 --- a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts @@ -12,11 +12,10 @@ import { ITextModel } from 'vs/editor/common/model'; import { IRange, Range } from 'vs/editor/common/core/range'; import { AbstractEditorNavigationQuickAccessProvider, IEditorNavigationQuickAccessOptions, IQuickAccessTextEditorContext } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess'; import { DocumentSymbol, SymbolKinds, SymbolTag, DocumentSymbolProviderRegistry, SymbolKind } from 'vs/editor/common/modes'; -import { OutlineModel, OutlineElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { OutlineModel } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { trim, format } from 'vs/base/common/strings'; import { prepareQuery, IPreparedQuery, pieceToQuery, scoreFuzzy2 } from 'vs/base/common/fuzzyScorer'; import { IMatch } from 'vs/base/common/filters'; -import { Iterable } from 'vs/base/common/iterator'; import { Codicon } from 'vs/base/common/codicons'; export interface IGotoSymbolQuickPickItem extends IQuickPickItem { @@ -144,7 +143,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit // Resolve symbols from document once and reuse this // request for all filtering and typing then on - const symbolsPromise = this.getDocumentSymbols(model, true, token); + const symbolsPromise = this.getDocumentSymbols(model, token); // Set initial picks and update on type let picksCts: CancellationTokenSource | undefined = undefined; @@ -418,49 +417,9 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit return result; } - protected async getDocumentSymbols(document: ITextModel, flatten: boolean, token: CancellationToken): Promise { + protected async getDocumentSymbols(document: ITextModel, token: CancellationToken): Promise { const model = await OutlineModel.create(document, token); - if (token.isCancellationRequested) { - return []; - } - - const roots: DocumentSymbol[] = []; - for (const child of model.children.values()) { - if (child instanceof OutlineElement) { - roots.push(child.symbol); - } else { - roots.push(...Iterable.map(child.children.values(), child => child.symbol)); - } - } - - let flatEntries: DocumentSymbol[] = []; - if (flatten) { - this.flattenDocumentSymbols(flatEntries, roots, ''); - } else { - flatEntries = roots; - } - - return flatEntries.sort((symbolA, symbolB) => Range.compareRangesUsingStarts(symbolA.range, symbolB.range)); - } - - private flattenDocumentSymbols(bucket: DocumentSymbol[], entries: DocumentSymbol[], overrideContainerLabel: string): void { - for (const entry of entries) { - bucket.push({ - kind: entry.kind, - tags: entry.tags, - name: entry.name, - detail: entry.detail, - containerName: entry.containerName || overrideContainerLabel, - range: entry.range, - selectionRange: entry.selectionRange, - children: undefined, // we flatten it... - }); - - // Recurse over children - if (entry.children) { - this.flattenDocumentSymbols(bucket, entry.children, entry.name); - } - } + return token.isCancellationRequested ? [] : model.asListOfDocumentSymbols(); } } diff --git a/src/vs/editor/contrib/smartSelect/bracketSelections.ts b/src/vs/editor/contrib/smartSelect/bracketSelections.ts index 3842ad87..af7496f4 100644 --- a/src/vs/editor/contrib/smartSelect/bracketSelections.ts +++ b/src/vs/editor/contrib/smartSelect/bracketSelections.ts @@ -26,7 +26,7 @@ export class BracketSelectionRangeProvider implements SelectionRangeProvider { return result; } - private static readonly _maxDuration = 30; + public static _maxDuration = 30; private static readonly _maxRounds = 2; private static _bracketsRightYield(resolve: () => void, round: number, model: ITextModel, pos: Position, ranges: Map>): void { diff --git a/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts b/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts index 4c566492..862a0feb 100644 --- a/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts +++ b/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts @@ -16,7 +16,7 @@ import { BracketSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/bra import { provideSelectionRanges } from 'vs/editor/contrib/smartSelect/smartSelect'; import { CancellationToken } from 'vs/base/common/cancellation'; import { WordSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/wordSelections'; -import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/modelService.test'; +import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/testTextResourcePropertiesService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { NullLogService } from 'vs/platform/log/common/log'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; @@ -45,6 +45,16 @@ class MockJSMode extends MockMode { suite('SmartSelect', () => { + const OriginalBracketSelectionRangeProviderMaxDuration = BracketSelectionRangeProvider._maxDuration; + + suiteSetup(() => { + BracketSelectionRangeProvider._maxDuration = 5000; // 5 seconds + }); + + suiteTeardown(() => { + BracketSelectionRangeProvider._maxDuration = OriginalBracketSelectionRangeProviderMaxDuration; + }); + let modelService: ModelServiceImpl; let mode: MockJSMode; diff --git a/src/vs/editor/contrib/snippet/snippetVariables.ts b/src/vs/editor/contrib/snippet/snippetVariables.ts index bdfe96e7..0113f863 100644 --- a/src/vs/editor/contrib/snippet/snippetVariables.ts +++ b/src/vs/editor/contrib/snippet/snippetVariables.ts @@ -12,11 +12,11 @@ import { VariableResolver, Variable, Text } from 'vs/editor/contrib/snippet/snip import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { getLeadingWhitespace, commonPrefixLength, isFalsyOrWhitespace, splitLines } from 'vs/base/common/strings'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier, WORKSPACE_EXTENSION, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { toWorkspaceIdentifier, WORKSPACE_EXTENSION, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { ILabelService } from 'vs/platform/label/common/label'; import { normalizeDriveLetter } from 'vs/base/common/labels'; -import { URI } from 'vs/base/common/uri'; import { OvertypingCapturer } from 'vs/editor/contrib/suggest/suggestOvertypingCapturer'; +import { generateUuid } from 'vs/base/common/uuid'; export const KnownSnippetVariableNames: { [key: string]: true } = Object.freeze({ 'CURRENT_YEAR': true, @@ -42,6 +42,7 @@ export const KnownSnippetVariableNames: { [key: string]: true } = Object.freeze( 'TM_FILENAME_BASE': true, 'TM_DIRECTORY': true, 'TM_FILEPATH': true, + 'RELATIVE_FILEPATH': true, 'BLOCK_COMMENT_START': true, 'BLOCK_COMMENT_END': true, 'LINE_COMMENT': true, @@ -49,6 +50,7 @@ export const KnownSnippetVariableNames: { [key: string]: true } = Object.freeze( 'WORKSPACE_FOLDER': true, 'RANDOM': true, 'RANDOM_HEX': true, + 'UUID': true }); export class CompositeSnippetVariableResolver implements VariableResolver { @@ -177,6 +179,8 @@ export class ModelBasedVariableResolver implements VariableResolver { } else if (name === 'TM_FILEPATH' && this._labelService) { return this._labelService.getUriLabel(this._model.uri); + } else if (name === 'RELATIVE_FILEPATH' && this._labelService) { + return this._labelService.getUriLabel(this._model.uri, { relative: true, noPrefix: true }); } return undefined; @@ -309,9 +313,9 @@ export class WorkspaceBasedVariableResolver implements VariableResolver { return undefined; } - private _resolveWorkspaceName(workspaceIdentifier: IWorkspaceIdentifier | URI): string | undefined { + private _resolveWorkspaceName(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string | undefined { if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) { - return path.basename(workspaceIdentifier.path); + return path.basename(workspaceIdentifier.uri.path); } let filename = path.basename(workspaceIdentifier.configPath.path); @@ -320,9 +324,9 @@ export class WorkspaceBasedVariableResolver implements VariableResolver { } return filename; } - private _resoveWorkspacePath(workspaceIdentifier: IWorkspaceIdentifier | URI): string | undefined { + private _resoveWorkspacePath(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string | undefined { if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) { - return normalizeDriveLetter(workspaceIdentifier.fsPath); + return normalizeDriveLetter(workspaceIdentifier.uri.fsPath); } let filename = path.basename(workspaceIdentifier.configPath.path); @@ -340,9 +344,10 @@ export class RandomBasedVariableResolver implements VariableResolver { if (name === 'RANDOM') { return Math.random().toString().slice(-6); - } - else if (name === 'RANDOM_HEX') { + } else if (name === 'RANDOM_HEX') { return Math.random().toString(16).slice(-6); + } else if (name === 'UUID') { + return generateUuid(); } return undefined; diff --git a/src/vs/editor/contrib/snippet/test/snippetController2.old.test.ts b/src/vs/editor/contrib/snippet/test/snippetController2.old.test.ts index 8c9be33c..2559a421 100644 --- a/src/vs/editor/contrib/snippet/test/snippetController2.old.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetController2.old.test.ts @@ -62,34 +62,34 @@ suite('SnippetController', () => { editor.setPosition({ lineNumber: 4, column: 2 }); snippetController.insert(template); - assert.equal(editor.getModel()!.getLineContent(4), '\tfor (var index; index < array.length; index++) {'); - assert.equal(editor.getModel()!.getLineContent(5), '\t\tvar element = array[index];'); - assert.equal(editor.getModel()!.getLineContent(6), '\t\t'); - assert.equal(editor.getModel()!.getLineContent(7), '\t}'); + assert.strictEqual(editor.getModel()!.getLineContent(4), '\tfor (var index; index < array.length; index++) {'); + assert.strictEqual(editor.getModel()!.getLineContent(5), '\t\tvar element = array[index];'); + assert.strictEqual(editor.getModel()!.getLineContent(6), '\t\t'); + assert.strictEqual(editor.getModel()!.getLineContent(7), '\t}'); editor.trigger('test', 'type', { text: 'i' }); - assert.equal(editor.getModel()!.getLineContent(4), '\tfor (var i; i < array.length; i++) {'); - assert.equal(editor.getModel()!.getLineContent(5), '\t\tvar element = array[i];'); - assert.equal(editor.getModel()!.getLineContent(6), '\t\t'); - assert.equal(editor.getModel()!.getLineContent(7), '\t}'); + assert.strictEqual(editor.getModel()!.getLineContent(4), '\tfor (var i; i < array.length; i++) {'); + assert.strictEqual(editor.getModel()!.getLineContent(5), '\t\tvar element = array[i];'); + assert.strictEqual(editor.getModel()!.getLineContent(6), '\t\t'); + assert.strictEqual(editor.getModel()!.getLineContent(7), '\t}'); snippetController.next(); editor.trigger('test', 'type', { text: 'arr' }); - assert.equal(editor.getModel()!.getLineContent(4), '\tfor (var i; i < arr.length; i++) {'); - assert.equal(editor.getModel()!.getLineContent(5), '\t\tvar element = arr[i];'); - assert.equal(editor.getModel()!.getLineContent(6), '\t\t'); - assert.equal(editor.getModel()!.getLineContent(7), '\t}'); + assert.strictEqual(editor.getModel()!.getLineContent(4), '\tfor (var i; i < arr.length; i++) {'); + assert.strictEqual(editor.getModel()!.getLineContent(5), '\t\tvar element = arr[i];'); + assert.strictEqual(editor.getModel()!.getLineContent(6), '\t\t'); + assert.strictEqual(editor.getModel()!.getLineContent(7), '\t}'); snippetController.prev(); editor.trigger('test', 'type', { text: 'j' }); - assert.equal(editor.getModel()!.getLineContent(4), '\tfor (var j; j < arr.length; j++) {'); - assert.equal(editor.getModel()!.getLineContent(5), '\t\tvar element = arr[j];'); - assert.equal(editor.getModel()!.getLineContent(6), '\t\t'); - assert.equal(editor.getModel()!.getLineContent(7), '\t}'); + assert.strictEqual(editor.getModel()!.getLineContent(4), '\tfor (var j; j < arr.length; j++) {'); + assert.strictEqual(editor.getModel()!.getLineContent(5), '\t\tvar element = arr[j];'); + assert.strictEqual(editor.getModel()!.getLineContent(6), '\t\t'); + assert.strictEqual(editor.getModel()!.getLineContent(7), '\t}'); snippetController.next(); snippetController.next(); - assert.deepEqual(editor.getPosition(), new Position(6, 3)); + assert.deepStrictEqual(editor.getPosition(), new Position(6, 3)); }); }); @@ -98,13 +98,13 @@ suite('SnippetController', () => { editor.setPosition({ lineNumber: 4, column: 2 }); snippetController.insert(template); - assert.equal(editor.getModel()!.getLineContent(4), '\tfor (var index; index < array.length; index++) {'); - assert.equal(editor.getModel()!.getLineContent(5), '\t\tvar element = array[index];'); - assert.equal(editor.getModel()!.getLineContent(6), '\t\t'); - assert.equal(editor.getModel()!.getLineContent(7), '\t}'); + assert.strictEqual(editor.getModel()!.getLineContent(4), '\tfor (var index; index < array.length; index++) {'); + assert.strictEqual(editor.getModel()!.getLineContent(5), '\t\tvar element = array[index];'); + assert.strictEqual(editor.getModel()!.getLineContent(6), '\t\t'); + assert.strictEqual(editor.getModel()!.getLineContent(7), '\t}'); snippetController.cancel(); - assert.deepEqual(editor.getPosition(), new Position(4, 16)); + assert.deepStrictEqual(editor.getPosition(), new Position(4, 16)); }); }); @@ -121,7 +121,7 @@ suite('SnippetController', () => { // text: null // }]); - // assert.equal(snippetController.isInSnippetMode(), false); + // assert.strictEqual(snippetController.isInSnippetMode(), false); // }); // }); @@ -138,7 +138,7 @@ suite('SnippetController', () => { // text: null // }]); - // assert.equal(snippetController.isInSnippetMode(), false); + // assert.strictEqual(snippetController.isInSnippetMode(), false); // }); // }); @@ -155,7 +155,7 @@ suite('SnippetController', () => { // text: '\nHello' // }]); - // assert.equal(snippetController.isInSnippetMode(), false); + // assert.strictEqual(snippetController.isInSnippetMode(), false); // }); // }); @@ -172,7 +172,7 @@ suite('SnippetController', () => { // text: '\nHello' // }]); - // assert.equal(snippetController.isInSnippetMode(), false); + // assert.strictEqual(snippetController.isInSnippetMode(), false); // }); // }); @@ -183,7 +183,7 @@ suite('SnippetController', () => { editor.getModel()!.setValue('goodbye'); - assert.equal(snippetController.isInSnippetMode(), false); + assert.strictEqual(snippetController.isInSnippetMode(), false); }); }); @@ -194,7 +194,7 @@ suite('SnippetController', () => { editor.getModel()!.undo(); - assert.equal(snippetController.isInSnippetMode(), false); + assert.strictEqual(snippetController.isInSnippetMode(), false); }); }); @@ -205,7 +205,7 @@ suite('SnippetController', () => { editor.setPosition({ lineNumber: 1, column: 1 }); - assert.equal(snippetController.isInSnippetMode(), false); + assert.strictEqual(snippetController.isInSnippetMode(), false); }); }); @@ -216,7 +216,7 @@ suite('SnippetController', () => { editor.setModel(null); - assert.equal(snippetController.isInSnippetMode(), false); + assert.strictEqual(snippetController.isInSnippetMode(), false); }); }); @@ -227,7 +227,7 @@ suite('SnippetController', () => { snippetController.dispose(); - assert.equal(snippetController.isInSnippetMode(), false); + assert.strictEqual(snippetController.isInSnippetMode(), false); }); }); @@ -241,7 +241,7 @@ suite('SnippetController', () => { codeSnippet = 'foo$0'; snippetController.insert(codeSnippet); - assert.equal(editor.getSelections()!.length, 2); + assert.strictEqual(editor.getSelections()!.length, 2); const [first, second] = editor.getSelections()!; assert.ok(first.equalsRange({ startLineNumber: 1, startColumn: 4, endLineNumber: 1, endColumn: 4 }), first.toString()); assert.ok(second.equalsRange({ startLineNumber: 2, startColumn: 4, endLineNumber: 2, endColumn: 4 }), second.toString()); @@ -256,7 +256,7 @@ suite('SnippetController', () => { codeSnippet = 'foo$0bar'; snippetController.insert(codeSnippet); - assert.equal(editor.getSelections()!.length, 2); + assert.strictEqual(editor.getSelections()!.length, 2); const [first, second] = editor.getSelections()!; assert.ok(first.equalsRange({ startLineNumber: 1, startColumn: 4, endLineNumber: 1, endColumn: 4 }), first.toString()); assert.ok(second.equalsRange({ startLineNumber: 2, startColumn: 4, endLineNumber: 2, endColumn: 4 }), second.toString()); @@ -271,7 +271,7 @@ suite('SnippetController', () => { codeSnippet = 'foo$0bar'; snippetController.insert(codeSnippet); - assert.equal(editor.getSelections()!.length, 2); + assert.strictEqual(editor.getSelections()!.length, 2); const [first, second] = editor.getSelections()!; assert.ok(first.equalsRange({ startLineNumber: 1, startColumn: 4, endLineNumber: 1, endColumn: 4 }), first.toString()); assert.ok(second.equalsRange({ startLineNumber: 1, startColumn: 14, endLineNumber: 1, endColumn: 14 }), second.toString()); @@ -286,7 +286,7 @@ suite('SnippetController', () => { codeSnippet = 'foo\n$0\nbar'; snippetController.insert(codeSnippet); - assert.equal(editor.getSelections()!.length, 2); + assert.strictEqual(editor.getSelections()!.length, 2); const [first, second] = editor.getSelections()!; assert.ok(first.equalsRange({ startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 1 }), first.toString()); assert.ok(second.equalsRange({ startLineNumber: 4, startColumn: 1, endLineNumber: 4, endColumn: 1 }), second.toString()); @@ -301,7 +301,7 @@ suite('SnippetController', () => { codeSnippet = 'foo\n$0\nbar'; snippetController.insert(codeSnippet); - assert.equal(editor.getSelections()!.length, 2); + assert.strictEqual(editor.getSelections()!.length, 2); const [first, second] = editor.getSelections()!; assert.ok(first.equalsRange({ startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 1 }), first.toString()); assert.ok(second.equalsRange({ startLineNumber: 4, startColumn: 1, endLineNumber: 4, endColumn: 1 }), second.toString()); @@ -315,7 +315,7 @@ suite('SnippetController', () => { codeSnippet = 'xo$0r'; snippetController.insert(codeSnippet, { overwriteBefore: 1 }); - assert.equal(editor.getSelections()!.length, 1); + assert.strictEqual(editor.getSelections()!.length, 1); assert.ok(editor.getSelection()!.equalsRange({ startLineNumber: 2, startColumn: 8, endColumn: 8, endLineNumber: 2 })); }); }); @@ -328,9 +328,9 @@ suite('SnippetController', () => { codeSnippet = '{{% url_**$1** %}}'; controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getSelections()!.length, 1); + assert.strictEqual(editor.getSelections()!.length, 1); assert.ok(editor.getSelection()!.equalsRange({ startLineNumber: 1, startColumn: 27, endLineNumber: 1, endColumn: 27 })); - assert.equal(editor.getModel()!.getValue(), 'example example {{% url_**** %}}'); + assert.strictEqual(editor.getModel()!.getValue(), 'example example {{% url_**** %}}'); }, ['example example sc']); @@ -346,9 +346,9 @@ suite('SnippetController', () => { controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getSelections()!.length, 1); + assert.strictEqual(editor.getSelections()!.length, 1); assert.ok(editor.getSelection()!.equalsRange({ startLineNumber: 2, startColumn: 2, endLineNumber: 2, endColumn: 2 }), editor.getSelection()!.toString()); - assert.equal(editor.getModel()!.getValue(), 'afterEach((done) => {\n\ttest\n});'); + assert.strictEqual(editor.getModel()!.getValue(), 'afterEach((done) => {\n\ttest\n});'); }, ['af']); @@ -364,9 +364,9 @@ suite('SnippetController', () => { controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getSelections()!.length, 1); + assert.strictEqual(editor.getSelections()!.length, 1); assert.ok(editor.getSelection()!.equalsRange({ startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 1 }), editor.getSelection()!.toString()); - assert.equal(editor.getModel()!.getValue(), 'afterEach((done) => {\n\ttest\n});'); + assert.strictEqual(editor.getModel()!.getValue(), 'afterEach((done) => {\n\ttest\n});'); }, ['af']); @@ -380,8 +380,8 @@ suite('SnippetController', () => { controller.insert(codeSnippet, { overwriteBefore: 8 }); - assert.equal(editor.getModel()!.getValue(), 'after'); - assert.equal(editor.getSelections()!.length, 1); + assert.strictEqual(editor.getModel()!.getValue(), 'after'); + assert.strictEqual(editor.getSelections()!.length, 1); assert.ok(editor.getSelection()!.equalsRange({ startLineNumber: 1, startColumn: 4, endLineNumber: 1, endColumn: 4 }), editor.getSelection()!.toString()); }, ['afterone']); @@ -404,7 +404,7 @@ suite('SnippetController', () => { controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getSelections()!.length, 2); + assert.strictEqual(editor.getSelections()!.length, 2); const [first, second] = editor.getSelections()!; assert.ok(first.equalsRange({ startLineNumber: 5, startColumn: 3, endLineNumber: 5, endColumn: 3 }), first.toString()); @@ -429,7 +429,7 @@ suite('SnippetController', () => { controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getSelections()!.length, 1); + assert.strictEqual(editor.getSelections()!.length, 1); const [first] = editor.getSelections()!; assert.ok(first.equalsRange({ startLineNumber: 2, startColumn: 3, endLineNumber: 2, endColumn: 3 }), first.toString()); @@ -465,7 +465,7 @@ suite('SnippetController', () => { codeSnippet = '_foo'; controller.insert(codeSnippet, { overwriteBefore: 1 }); - assert.equal(editor.getModel()!.getValue(), 'this._foo\nabc_foo'); + assert.strictEqual(editor.getModel()!.getValue(), 'this._foo\nabc_foo'); }, ['this._', 'abc']); @@ -478,7 +478,7 @@ suite('SnippetController', () => { codeSnippet = 'XX'; controller.insert(codeSnippet, { overwriteBefore: 1 }); - assert.equal(editor.getModel()!.getValue(), 'this.XX\nabcXX'); + assert.strictEqual(editor.getModel()!.getValue(), 'this.XX\nabcXX'); }, ['this._', 'abc']); @@ -492,7 +492,7 @@ suite('SnippetController', () => { codeSnippet = '_foo'; controller.insert(codeSnippet, { overwriteBefore: 1 }); - assert.equal(editor.getModel()!.getValue(), 'this._foo\nabc_foo\ndef_foo'); + assert.strictEqual(editor.getModel()!.getValue(), 'this._foo\nabc_foo\ndef_foo'); }, ['this._', 'abc', 'def_']); @@ -506,7 +506,7 @@ suite('SnippetController', () => { codeSnippet = '._foo'; controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getModel()!.getValue(), 'this._foo\nabc._foo\ndef._foo'); + assert.strictEqual(editor.getModel()!.getValue(), 'this._foo\nabc._foo\ndef._foo'); }, ['this._', 'abc', 'def._']); @@ -520,7 +520,7 @@ suite('SnippetController', () => { codeSnippet = '._foo'; controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getModel()!.getValue(), 'this._foo\nabc._foo\ndef._foo'); + assert.strictEqual(editor.getModel()!.getValue(), 'this._foo\nabc._foo\ndef._foo'); }, ['this._', 'abc', 'def._']); @@ -534,7 +534,7 @@ suite('SnippetController', () => { codeSnippet = '._foo'; controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getModel()!.getValue(), 'this._._foo\na._foo\ndef._._foo'); + assert.strictEqual(editor.getModel()!.getValue(), 'this._._foo\na._foo\ndef._._foo'); }, ['this._', 'abc', 'def._']); @@ -550,7 +550,7 @@ suite('SnippetController', () => { codeSnippet = 'document'; controller.insert(codeSnippet, { overwriteBefore: 3 }); - assert.equal(editor.getModel()!.getValue(), '{document}\n{document && true}'); + assert.strictEqual(editor.getModel()!.getValue(), '{document}\n{document && true}'); }, ['{foo}', '{foo && true}']); }); @@ -565,7 +565,7 @@ suite('SnippetController', () => { codeSnippet = 'for (var ${1:i}=0; ${1:i} { codeSnippet = 'for (let ${1:i}=0; ${1:i} expected=${actual.toString()}`); } - assert.equal(s.length, 0); + assert.strictEqual(s.length, 0); } function assertContextKeys(service: MockContextKeyService, inSnippet: boolean, hasPrev: boolean, hasNext: boolean): void { - assert.equal(SnippetController2.InSnippetMode.getValue(service), inSnippet, `inSnippetMode`); - assert.equal(SnippetController2.HasPrevTabstop.getValue(service), hasPrev, `HasPrevTabstop`); - assert.equal(SnippetController2.HasNextTabstop.getValue(service), hasNext, `HasNextTabstop`); + assert.strictEqual(SnippetController2.InSnippetMode.getValue(service), inSnippet, `inSnippetMode`); + assert.strictEqual(SnippetController2.HasPrevTabstop.getValue(service), hasPrev, `HasPrevTabstop`); + assert.strictEqual(SnippetController2.HasNextTabstop.getValue(service), hasNext, `HasNextTabstop`); } let editor: ICodeEditor; @@ -40,7 +40,7 @@ suite('SnippetController2', function () { model = createTextModel('if\n $state\nfi'); editor = createTestCodeEditor({ model: model }); editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)]); - assert.equal(model.getEOL(), '\n'); + assert.strictEqual(model.getEOL(), '\n'); }); teardown(function () { @@ -78,9 +78,9 @@ suite('SnippetController2', function () { assertContextKeys(contextKeys, false, false, false); editor.trigger('test', 'type', { text: '\t' }); - assert.equal(SnippetController2.InSnippetMode.getValue(contextKeys), false); - assert.equal(SnippetController2.HasNextTabstop.getValue(contextKeys), false); - assert.equal(SnippetController2.HasPrevTabstop.getValue(contextKeys), false); + assert.strictEqual(SnippetController2.InSnippetMode.getValue(contextKeys), false); + assert.strictEqual(SnippetController2.HasNextTabstop.getValue(contextKeys), false); + assert.strictEqual(SnippetController2.HasPrevTabstop.getValue(contextKeys), false); }); test('insert, insert -> cursor moves out (left/right)', function () { @@ -111,7 +111,7 @@ suite('SnippetController2', function () { const ctrl = new SnippetController2(editor, logService, contextKeys); ctrl.insert('foo${1:bar}foo$0'); - assert.equal(SnippetController2.InSnippetMode.getValue(contextKeys), true); + assert.strictEqual(SnippetController2.InSnippetMode.getValue(contextKeys), true); assertSelections(editor, new Selection(1, 4, 1, 7), new Selection(2, 8, 2, 11)); // bad selection change diff --git a/src/vs/editor/contrib/snippet/test/snippetParser.test.ts b/src/vs/editor/contrib/snippet/test/snippetParser.test.ts index ba019cdc..ed5dcc44 100644 --- a/src/vs/editor/contrib/snippet/test/snippetParser.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetParser.test.ts @@ -531,8 +531,8 @@ suite('SnippetParser', () => { let snippet = new SnippetParser().parse('This ${1:is ${2:nested}}$0', true); let [first, second] = snippet.placeholders; - assert.deepEqual(snippet.enclosingPlaceholders(first), []); - assert.deepEqual(snippet.enclosingPlaceholders(second), [first]); + assert.deepStrictEqual(snippet.enclosingPlaceholders(first), []); + assert.deepStrictEqual(snippet.enclosingPlaceholders(second), [first]); }); test('TextmateSnippet#offset', () => { diff --git a/src/vs/editor/contrib/snippet/test/snippetSession.test.ts b/src/vs/editor/contrib/snippet/test/snippetSession.test.ts index 458555cb..a65de3d5 100644 --- a/src/vs/editor/contrib/snippet/test/snippetSession.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetSession.test.ts @@ -23,14 +23,14 @@ suite('SnippetSession', function () { const actual = s.shift()!; assert.ok(selection.equalsSelection(actual), `actual=${selection.toString()} <> expected=${actual.toString()}`); } - assert.equal(s.length, 0); + assert.strictEqual(s.length, 0); } setup(function () { model = createTextModel('function foo() {\n console.log(a);\n}'); editor = createTestCodeEditor({ model: model }) as IActiveCodeEditor; editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)]); - assert.equal(model.getEOL(), '\n'); + assert.strictEqual(model.getEOL(), '\n'); }); teardown(function () { @@ -43,7 +43,7 @@ suite('SnippetSession', function () { function assertNormalized(position: IPosition, input: string, expected: string): void { const snippet = new SnippetParser().parse(input); SnippetSession.adjustWhitespace(model, position, snippet, true, true); - assert.equal(snippet.toTextmateString(), expected); + assert.strictEqual(snippet.toTextmateString(), expected); } assertNormalized(new Position(1, 1), 'foo', 'foo'); @@ -73,7 +73,7 @@ suite('SnippetSession', function () { test('text edits & selection', function () { const session = new SnippetSession(editor, 'foo${1:bar}foo$0'); session.insert(); - assert.equal(editor.getModel()!.getValue(), 'foobarfoofunction foo() {\n foobarfooconsole.log(a);\n}'); + assert.strictEqual(editor.getModel()!.getValue(), 'foobarfoofunction foo() {\n foobarfooconsole.log(a);\n}'); assertSelections(editor, new Selection(1, 4, 1, 7), new Selection(2, 8, 2, 11)); session.next(); @@ -86,7 +86,7 @@ suite('SnippetSession', function () { editor.setSelections([new Selection(2, 5, 2, 5), new Selection(1, 1, 1, 1)]); session.insert(); - assert.equal(model.getValue(), 'barfunction foo() {\n barconsole.log(a);\n}'); + assert.strictEqual(model.getValue(), 'barfunction foo() {\n barconsole.log(a);\n}'); assertSelections(editor, new Selection(2, 5, 2, 8), new Selection(1, 1, 1, 4)); }); @@ -107,7 +107,7 @@ suite('SnippetSession', function () { test('snippets, just text', function () { const session = new SnippetSession(editor, 'foobar'); session.insert(); - assert.equal(model.getValue(), 'foobarfunction foo() {\n foobarconsole.log(a);\n}'); + assert.strictEqual(model.getValue(), 'foobarfunction foo() {\n foobarconsole.log(a);\n}'); assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11)); }); @@ -116,7 +116,7 @@ suite('SnippetSession', function () { const session = new SnippetSession(editor, 'foo\n\t${1:bar}\n$0'); session.insert(); - assert.equal(editor.getModel()!.getValue(), 'foo\n bar\nfunction foo() {\n foo\n bar\n console.log(a);\n}'); + assert.strictEqual(editor.getModel()!.getValue(), 'foo\n bar\nfunction foo() {\n foo\n bar\n console.log(a);\n}'); assertSelections(editor, new Selection(2, 5, 2, 8), new Selection(5, 9, 5, 12)); @@ -129,7 +129,7 @@ suite('SnippetSession', function () { editor.setSelection(new Selection(2, 5, 2, 5)); const session = new SnippetSession(editor, 'abc\n foo\n bar\n$0', { overwriteBefore: 0, overwriteAfter: 0, adjustWhitespace: false, clipboardText: undefined, overtypingCapturer: undefined }); session.insert(); - assert.equal(editor.getModel()!.getValue(), 'function foo() {\n abc\n foo\n bar\nconsole.log(a);\n}'); + assert.strictEqual(editor.getModel()!.getValue(), 'function foo() {\n abc\n foo\n bar\nconsole.log(a);\n}'); }); test('snippets, selections -> next/prev', () => { @@ -171,7 +171,7 @@ suite('SnippetSession', function () { // go to final tabstop session.next(); - assert.equal(model.getValue(), 'fX_bar_function foo() {\n fX_bar_console.log(a);\n}'); + assert.strictEqual(model.getValue(), 'fX_bar_function foo() {\n fX_bar_console.log(a);\n}'); assertSelections(editor, new Selection(1, 8, 1, 8), new Selection(2, 12, 2, 12)); }); @@ -180,7 +180,7 @@ suite('SnippetSession', function () { editor.setSelections([new Selection(1, 1, 1, 4), new Selection(1, 9, 1, 12)]); new SnippetSession(editor, 'x$0').insert(); - assert.equal(model.getValue(), 'x_bar_x'); + assert.strictEqual(model.getValue(), 'x_bar_x'); assertSelections(editor, new Selection(1, 2, 1, 2), new Selection(1, 8, 1, 8)); }); @@ -189,7 +189,7 @@ suite('SnippetSession', function () { editor.setSelections([new Selection(1, 1, 1, 4), new Selection(1, 9, 1, 12)]); new SnippetSession(editor, 'LONGER$0').insert(); - assert.equal(model.getValue(), 'LONGER_bar_LONGER'); + assert.strictEqual(model.getValue(), 'LONGER_bar_LONGER'); assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(1, 18, 1, 18)); }); @@ -203,11 +203,11 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: 'foo-' }); session.next(); - assert.equal(model.getValue(), 'foo_foo-bar_foo'); + assert.strictEqual(model.getValue(), 'foo_foo-bar_foo'); assertSelections(editor, new Selection(1, 12, 1, 12)); editor.trigger('test', 'type', { text: 'XXX' }); - assert.equal(model.getValue(), 'foo_foo-barXXX_foo'); + assert.strictEqual(model.getValue(), 'foo_foo-barXXX_foo'); session.prev(); assertSelections(editor, new Selection(1, 5, 1, 9)); session.next(); @@ -242,7 +242,7 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: '333' }); session.next(); - assert.equal(model.getValue(), '111222333function foo() {\n 111222333console.log(a);\n}'); + assert.strictEqual(model.getValue(), '111222333function foo() {\n 111222333console.log(a);\n}'); assertSelections(editor, new Selection(1, 10, 1, 10), new Selection(2, 14, 2, 14)); session.prev(); @@ -269,22 +269,22 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: '333' }); session.next(); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isAtLastPlaceholder, true); }); test('snippets, gracefully move over final tabstop', function () { const session = new SnippetSession(editor, '${1}bar$0'); session.insert(); - assert.equal(session.isAtLastPlaceholder, false); + assert.strictEqual(session.isAtLastPlaceholder, false); assertSelections(editor, new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)); session.next(); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 4, 1, 4), new Selection(2, 8, 2, 8)); session.next(); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 4, 1, 4), new Selection(2, 8, 2, 8)); }); @@ -294,46 +294,46 @@ suite('SnippetSession', function () { assertSelections(editor, new Selection(1, 5, 1, 7), new Selection(2, 9, 2, 11)); editor.trigger('test', 'type', { text: 'XXX' }); - assert.equal(model.getValue(), 'log(XXX);function foo() {\n log(XXX);console.log(a);\n}'); + assert.strictEqual(model.getValue(), 'log(XXX);function foo() {\n log(XXX);console.log(a);\n}'); session.next(); - assert.equal(session.isAtLastPlaceholder, false); + assert.strictEqual(session.isAtLastPlaceholder, false); // assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11)); session.next(); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 10, 1, 10), new Selection(2, 14, 2, 14)); }); test('snippets, selections and snippet ranges', function () { const session = new SnippetSession(editor, '${1:foo}farboo${2:bar}$0'); session.insert(); - assert.equal(model.getValue(), 'foofarboobarfunction foo() {\n foofarboobarconsole.log(a);\n}'); + assert.strictEqual(model.getValue(), 'foofarboobarfunction foo() {\n foofarboobarconsole.log(a);\n}'); assertSelections(editor, new Selection(1, 1, 1, 4), new Selection(2, 5, 2, 8)); - assert.equal(session.isSelectionWithinPlaceholders(), true); + assert.strictEqual(session.isSelectionWithinPlaceholders(), true); editor.setSelections([new Selection(1, 1, 1, 1)]); - assert.equal(session.isSelectionWithinPlaceholders(), false); + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); editor.setSelections([new Selection(1, 6, 1, 6), new Selection(2, 10, 2, 10)]); - assert.equal(session.isSelectionWithinPlaceholders(), false); // in snippet, outside placeholder + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); // in snippet, outside placeholder editor.setSelections([new Selection(1, 6, 1, 6), new Selection(2, 10, 2, 10), new Selection(1, 1, 1, 1)]); - assert.equal(session.isSelectionWithinPlaceholders(), false); // in snippet, outside placeholder + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); // in snippet, outside placeholder editor.setSelections([new Selection(1, 6, 1, 6), new Selection(2, 10, 2, 10), new Selection(2, 20, 2, 21)]); - assert.equal(session.isSelectionWithinPlaceholders(), false); + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); // reset selection to placeholder session.next(); - assert.equal(session.isSelectionWithinPlaceholders(), true); + assert.strictEqual(session.isSelectionWithinPlaceholders(), true); assertSelections(editor, new Selection(1, 10, 1, 13), new Selection(2, 14, 2, 17)); // reset selection to placeholder session.next(); - assert.equal(session.isSelectionWithinPlaceholders(), true); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isSelectionWithinPlaceholders(), true); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 13, 1, 13), new Selection(2, 17, 2, 17)); }); @@ -344,20 +344,20 @@ suite('SnippetSession', function () { const first = new SnippetSession(editor, 'foo${2:bar}foo$0'); first.insert(); - assert.equal(model.getValue(), 'foobarfoo'); + assert.strictEqual(model.getValue(), 'foobarfoo'); assertSelections(editor, new Selection(1, 4, 1, 7)); const second = new SnippetSession(editor, 'ba${1:zzzz}$0'); second.insert(); - assert.equal(model.getValue(), 'foobazzzzfoo'); + assert.strictEqual(model.getValue(), 'foobazzzzfoo'); assertSelections(editor, new Selection(1, 6, 1, 10)); second.next(); - assert.equal(second.isAtLastPlaceholder, true); + assert.strictEqual(second.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 10, 1, 10)); first.next(); - assert.equal(first.isAtLastPlaceholder, true); + assert.strictEqual(first.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 13, 1, 13)); }); @@ -365,11 +365,11 @@ suite('SnippetSession', function () { const session = new SnippetSession(editor, 'farboo$0'); session.insert(); - assert.equal(session.isAtLastPlaceholder, true); - assert.equal(session.isSelectionWithinPlaceholders(), false); + assert.strictEqual(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); editor.trigger('test', 'type', { text: 'XXX' }); - assert.equal(session.isSelectionWithinPlaceholders(), false); + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); }); test('snippets, typing at beginning', function () { @@ -379,12 +379,12 @@ suite('SnippetSession', function () { session.insert(); editor.setSelection(new Selection(1, 2, 1, 2)); - assert.equal(session.isSelectionWithinPlaceholders(), false); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); + assert.strictEqual(session.isAtLastPlaceholder, true); editor.trigger('test', 'type', { text: 'XXX' }); - assert.equal(model.getLineContent(1), 'fXXXfarboounction foo() {'); - assert.equal(session.isSelectionWithinPlaceholders(), false); + assert.strictEqual(model.getLineContent(1), 'fXXXfarboounction foo() {'); + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); session.next(); assertSelections(editor, new Selection(1, 11, 1, 11)); @@ -412,7 +412,7 @@ suite('SnippetSession', function () { const session = new SnippetSession(editor, '@line=$TM_LINE_NUMBER$0'); session.insert(); - assert.equal(model.getValue(), '@line=1function foo() {\n @line=2console.log(a);\n}'); + assert.strictEqual(model.getValue(), '@line=1function foo() {\n @line=2console.log(a);\n}'); assertSelections(editor, new Selection(1, 8, 1, 8), new Selection(2, 12, 2, 12)); }); @@ -428,10 +428,10 @@ suite('SnippetSession', function () { session.next(); assertSelections(editor, new Selection(1, 22, 1, 22)); - assert.equal(session.isAtLastPlaceholder, false); + assert.strictEqual(session.isAtLastPlaceholder, false); session.next(); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 23, 1, 23)); session.prev(); @@ -456,8 +456,8 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: 'foo' }); session.next(); - assert.equal(model.getValue(), 'bar'); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(model.getValue(), 'bar'); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 4, 1, 4)); }); @@ -471,8 +471,8 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: 'foo' }); session.next(); - assert.equal(model.getValue(), 'foo baz bar'); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(model.getValue(), 'foo baz bar'); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 12, 1, 12)); }); @@ -493,8 +493,8 @@ suite('SnippetSession', function () { assertSelections(editor, new Selection(1, 16, 1, 16)); session.next(); - assert.equal(model.getValue(), 'clk : std_logic;\n'); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(model.getValue(), 'clk : std_logic;\n'); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(2, 1, 2, 1)); }); @@ -532,8 +532,8 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: 'string' }); session.next(); - assert.equal(model.getValue(), expected); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(model.getValue(), expected); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(4, 2, 4, 2)); }); @@ -556,8 +556,8 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: ' := \'1\'' }); session.next(); - assert.equal(model.getValue(), 'clk : std_logic := \'1\';\n'); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(model.getValue(), 'clk : std_logic := \'1\';\n'); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(2, 1, 2, 1)); }); @@ -570,13 +570,13 @@ suite('SnippetSession', function () { assertSelections(editor, new Selection(1, 1, 1, 2), new Selection(1, 5, 1, 6)); session.next(); - assert.equal(model.getValue(), '{fff}'); + assert.strictEqual(model.getValue(), '{fff}'); assertSelections(editor, new Selection(1, 2, 1, 5)); editor.trigger('test', 'type', { text: 'ggg' }); session.next(); - assert.equal(model.getValue(), '{ggg}'); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(model.getValue(), '{ggg}'); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 6, 1, 6)); }); @@ -584,7 +584,7 @@ suite('SnippetSession', function () { editor.getModel().setValue(''); const session = new SnippetSession(editor, '${1:{}${2:fff}${1/[\\{]/}/}$0'); session.insert(); - assert.equal(editor.getModel().getValue(), '{fff{'); + assert.strictEqual(editor.getModel().getValue(), '{fff{'); assertSelections(editor, new Selection(1, 1, 1, 2), new Selection(1, 5, 1, 6)); session.next(); @@ -599,25 +599,25 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: '1' }); editor.trigger('test', 'type', { text: '\n' }); - assert.equal(editor.getModel()!.getValue(), 'test 1\n'); + assert.strictEqual(editor.getModel()!.getValue(), 'test 1\n'); session.merge('test ${1:replaceme}'); editor.trigger('test', 'type', { text: '2' }); editor.trigger('test', 'type', { text: '\n' }); - assert.equal(editor.getModel()!.getValue(), 'test 1\ntest 2\n'); + assert.strictEqual(editor.getModel()!.getValue(), 'test 1\ntest 2\n'); session.merge('test ${1:replaceme}'); editor.trigger('test', 'type', { text: '3' }); editor.trigger('test', 'type', { text: '\n' }); - assert.equal(editor.getModel()!.getValue(), 'test 1\ntest 2\ntest 3\n'); + assert.strictEqual(editor.getModel()!.getValue(), 'test 1\ntest 2\ntest 3\n'); session.merge('test ${1:replaceme}'); editor.trigger('test', 'type', { text: '4' }); editor.trigger('test', 'type', { text: '\n' }); - assert.equal(editor.getModel()!.getValue(), 'test 1\ntest 2\ntest 3\ntest 4\n'); + assert.strictEqual(editor.getModel()!.getValue(), 'test 1\ntest 2\ntest 3\ntest 4\n'); }); test('Snippet variable text isn\'t whitespace normalised, #31124', function () { @@ -642,7 +642,7 @@ suite('SnippetSession', function () { 'end' ].join('\n'); - assert.equal(editor.getModel()!.getValue(), expected); + assert.strictEqual(editor.getModel()!.getValue(), expected); editor.getModel()!.setValue([ 'start', @@ -665,7 +665,7 @@ suite('SnippetSession', function () { 'end' ].join('\n'); - assert.equal(editor.getModel()!.getValue(), expected); + assert.strictEqual(editor.getModel()!.getValue(), expected); }); test('Selecting text from left to right, and choosing item messes up code, #31199', function () { @@ -680,7 +680,7 @@ suite('SnippetSession', function () { editor.setSelections([new Selection(1, 9, 1, 12)]); new SnippetSession(editor, 'far', { overwriteBefore: 3, overwriteAfter: 0, adjustWhitespace: true, clipboardText: undefined, overtypingCapturer: undefined }).insert(); - assert.equal(model.getValue(), 'console.far'); + assert.strictEqual(model.getValue(), 'console.far'); }); test('Tabs don\'t get replaced with spaces in snippet transformations #103818', function () { diff --git a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts index 8e939a5a..dff2a64b 100644 --- a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts @@ -9,11 +9,14 @@ import { Selection } from 'vs/editor/common/core/selection'; import { SelectionBasedVariableResolver, CompositeSnippetVariableResolver, ModelBasedVariableResolver, ClipboardBasedVariableResolver, TimeBasedVariableResolver, WorkspaceBasedVariableResolver } from 'vs/editor/contrib/snippet/snippetVariables'; import { SnippetParser, Variable, VariableResolver } from 'vs/editor/contrib/snippet/snippetParser'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { toWorkspaceFolders, IWorkspace, IWorkspaceContextService, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IWorkspace, IWorkspaceContextService, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ILabelService } from 'vs/platform/label/common/label'; import { mock } from 'vs/base/test/common/mock'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { sep } from 'vs/base/common/path'; +import { toWorkspaceFolders } from 'vs/platform/workspaces/common/workspaces'; suite('Snippet Variables Resolver', function () { @@ -48,9 +51,9 @@ suite('Snippet Variables Resolver', function () { const variable = snippet.children[0]; variable.resolve(resolver); if (variable.children.length === 0) { - assert.equal(undefined, expected); + assert.strictEqual(undefined, expected); } else { - assert.equal(variable.toString(), expected); + assert.strictEqual(variable.toString(), expected); } } @@ -127,17 +130,17 @@ suite('Snippet Variables Resolver', function () { test('TextmateSnippet, resolve variable', function () { const snippet = new SnippetParser().parse('"$TM_CURRENT_WORD"', true); - assert.equal(snippet.toString(), '""'); + assert.strictEqual(snippet.toString(), '""'); snippet.resolveVariables(resolver); - assert.equal(snippet.toString(), '"this"'); + assert.strictEqual(snippet.toString(), '"this"'); }); test('TextmateSnippet, resolve variable with default', function () { const snippet = new SnippetParser().parse('"${TM_CURRENT_WORD:foo}"', true); - assert.equal(snippet.toString(), '"foo"'); + assert.strictEqual(snippet.toString(), '"foo"'); snippet.resolveVariables(resolver); - assert.equal(snippet.toString(), '"this"'); + assert.strictEqual(snippet.toString(), '"this"'); }); test('More useful environment variables for snippets, #32737', function () { @@ -169,14 +172,14 @@ suite('Snippet Variables Resolver', function () { .resolveVariables({ resolve(variable) { return varValue || variable.name; } }); const actual = snippet.toString(); - assert.equal(actual, expected); + assert.strictEqual(actual, expected); } test('Variable Snippet Transform', function () { const snippet = new SnippetParser().parse('name=${TM_FILENAME/(.*)\\..+$/$1/}', true); snippet.resolveVariables(resolver); - assert.equal(snippet.toString(), 'name=text'); + assert.strictEqual(snippet.toString(), 'name=text'); assertVariableResolve2('${ThisIsAVar/([A-Z]).*(Var)/$2/}', 'Var'); assertVariableResolve2('${ThisIsAVar/([A-Z]).*(Var)/$2-${1:/downcase}/}', 'Var-t'); @@ -267,7 +270,7 @@ suite('Snippet Variables Resolver', function () { const snippet = new SnippetParser().parse(`$${varName}`); const variable = snippet.children[0]; - assert.equal(variable.resolve(resolver), true, `${varName} failed to resolve`); + assert.strictEqual(variable.resolve(resolver), true, `${varName} failed to resolve`); } test('Add time variables for snippets #41631, #43140', function () { @@ -292,10 +295,10 @@ suite('Snippet Variables Resolver', function () { const snippet = new SnippetParser().parse('${TM_LINE_NUMBER/(10)/${1:?It is:It is not}/} line 10', true); snippet.resolveVariables({ resolve() { return '10'; } }); - assert.equal(snippet.toString(), 'It is line 10'); + assert.strictEqual(snippet.toString(), 'It is line 10'); snippet.resolveVariables({ resolve() { return '11'; } }); - assert.equal(snippet.toString(), 'It is not line 10'); + assert.strictEqual(snippet.toString(), 'It is not line 10'); }); test('Add workspace name and folder variables for snippets #68261', function () { @@ -332,10 +335,55 @@ suite('Snippet Variables Resolver', function () { // workspace with config const workspaceConfigPath = URI.file('testWorkspace.code-workspace'); - workspace = new Workspace('', toWorkspaceFolders([{ path: 'folderName' }], workspaceConfigPath), workspaceConfigPath); + workspace = new Workspace('', toWorkspaceFolders([{ path: 'folderName' }], workspaceConfigPath, extUriBiasedIgnorePathCase), workspaceConfigPath); assertVariableResolve(resolver, 'WORKSPACE_NAME', 'testWorkspace'); if (!isWindows) { assertVariableResolve(resolver, 'WORKSPACE_FOLDER', '/'); } }); + + test('Add RELATIVE_FILEPATH snippet variable #114208', function () { + + let resolver: VariableResolver; + + // Mock a label service (only coded for file uris) + const workspaceLabelService = ((rootPath: string): ILabelService => { + const labelService = new class extends mock() { + getUriLabel(uri: URI, options: { relative?: boolean } = {}) { + const rootFsPath = URI.file(rootPath).fsPath + sep; + const fsPath = uri.fsPath; + if (options.relative && rootPath && fsPath.startsWith(rootFsPath)) { + return fsPath.substring(rootFsPath.length); + } + return fsPath; + } + }; + return labelService; + }); + + const model = createTextModel('', undefined, undefined, URI.parse('file:///foo/files/text.txt')); + + // empty workspace + resolver = new ModelBasedVariableResolver( + workspaceLabelService(''), + model + ); + + if (!isWindows) { + assertVariableResolve(resolver, 'RELATIVE_FILEPATH', '/foo/files/text.txt'); + } else { + assertVariableResolve(resolver, 'RELATIVE_FILEPATH', '\\foo\\files\\text.txt'); + } + + // single folder workspace + resolver = new ModelBasedVariableResolver( + workspaceLabelService('/foo'), + model + ); + if (!isWindows) { + assertVariableResolve(resolver, 'RELATIVE_FILEPATH', 'files/text.txt'); + } else { + assertVariableResolve(resolver, 'RELATIVE_FILEPATH', 'files\\text.txt'); + } + }); }); diff --git a/src/vs/editor/contrib/suggest/suggestModel.ts b/src/vs/editor/contrib/suggest/suggestModel.ts index 9a88e1cc..747f78a4 100644 --- a/src/vs/editor/contrib/suggest/suggestModel.ts +++ b/src/vs/editor/contrib/suggest/suggestModel.ts @@ -443,10 +443,6 @@ export class SuggestModel implements IDisposable { this._requestToken?.dispose(); - if (this._state === State.Idle) { - return; - } - if (!this._editor.hasModel()) { return; } @@ -456,6 +452,10 @@ export class SuggestModel implements IDisposable { clipboardText = await this._clipboardService.readText(); } + if (this._state === State.Idle) { + return; + } + const model = this._editor.getModel(); let items = completions.items; diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 02f6233f..158e3801 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -5,7 +5,6 @@ import 'vs/css!./media/suggest'; import 'vs/base/browser/ui/codicons/codiconStyles'; // The codicon symbol styles are defined here and must be loaded -import 'vs/editor/contrib/documentSymbols/outlineTree'; // The codicon symbol colors are defined here and must be loaded import * as nls from 'vs/nls'; import * as strings from 'vs/base/common/strings'; import * as dom from 'vs/base/browser/dom'; @@ -436,6 +435,7 @@ export class SuggestWidget implements IDisposable { case State.Hidden: dom.hide(this._messageElement, this._listElement, this._status.element); this._details.hide(true); + this._status.hide(); this._contentWidget.hide(); this._ctxSuggestWidgetVisible.reset(); this._ctxSuggestWidgetMultipleSuggestions.reset(); @@ -483,6 +483,7 @@ export class SuggestWidget implements IDisposable { } private _show(): void { + this._status.show(); this._contentWidget.show(); this._layout(this._persistedSize.restore()); this._ctxSuggestWidgetVisible.set(true); @@ -702,6 +703,14 @@ export class SuggestWidget implements IDisposable { this._loadingTimeout?.dispose(); this._setState(State.Hidden); this._onDidHide.fire(this); + + // ensure that a reasonable widget height is persisted so that + // accidential "resize-to-single-items" cases aren't happening + const dim = this._persistedSize.restore(); + const minPersistedHeight = Math.ceil(this.getLayoutInfo().itemHeight * 4.3); + if (dim && dim.height < minPersistedHeight) { + this._persistedSize.store(dim.with(undefined, minPersistedHeight)); + } } isFrozen(): boolean { @@ -734,12 +743,16 @@ export class SuggestWidget implements IDisposable { return; } - let height = size?.height; - let width = size?.width; - const bodyBox = dom.getClientArea(document.body); const info = this.getLayoutInfo(); + if (!size) { + size = info.defaultSize; + } + + let height = size.height; + let width = size.width; + // status bar this._status.element.style.lineHeight = `${info.itemHeight}px`; @@ -756,9 +769,6 @@ export class SuggestWidget implements IDisposable { // width math const maxWidth = bodyBox.width - info.borderHeight - 2 * info.horizontalPadding; - if (width === undefined) { - width = info.defaultSize.width; - } if (width > maxWidth) { width = maxWidth; } @@ -766,7 +776,6 @@ export class SuggestWidget implements IDisposable { // height math const fullHeight = info.statusBarHeight + this._list.contentHeight + info.borderHeight; - const preferredHeight = info.defaultSize.height; const minHeight = info.itemHeight + info.statusBarHeight; const editorBox = dom.getDomNodePagePosition(this.editor.getDomNode()); const cursorBox = this.editor.getScrolledVisiblePosition(this.editor.getPosition()); @@ -775,15 +784,12 @@ export class SuggestWidget implements IDisposable { const maxHeightAbove = Math.min(editorBox.top + cursorBox.top - info.verticalPadding, fullHeight); let maxHeight = Math.min(Math.max(maxHeightAbove, maxHeightBelow) + info.borderHeight, fullHeight); - if (height && height === this._cappedHeight?.capped) { + if (height === this._cappedHeight?.capped) { // Restore the old (wanted) height when the current // height is capped to fit height = this._cappedHeight.wanted; } - if (height === undefined) { - height = Math.min(preferredHeight, fullHeight); - } if (height < minHeight) { height = minHeight; } @@ -801,14 +807,14 @@ export class SuggestWidget implements IDisposable { this.element.enableSashes(false, true, true, false); maxHeight = maxHeightBelow; } - this.element.preferredSize = new dom.Dimension(preferredWidth, preferredHeight); + this.element.preferredSize = new dom.Dimension(preferredWidth, info.defaultSize.height); this.element.maxSize = new dom.Dimension(maxWidth, maxHeight); this.element.minSize = new dom.Dimension(220, minHeight); // Know when the height was capped to fit and remember // the wanted height for later. This is required when going // left to widen suggestions. - this._cappedHeight = size && height === fullHeight + this._cappedHeight = height === fullHeight ? { wanted: this._cappedHeight?.wanted ?? size.height, capped: height } : undefined; } diff --git a/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts b/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts index 26cdc77a..71098fe0 100644 --- a/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts +++ b/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts @@ -117,7 +117,7 @@ export class ItemRenderer implements IListRenderer(action => { - return action instanceof MenuItemAction - ? instantiationService.createInstance(StatusBarViewItem, action) - : undefined; + return action instanceof MenuItemAction ? instantiationService.createInstance(StatusBarViewItem, action) : undefined; }); - const leftActions = new ActionBar(this.element, { actionViewItemProvider }); - const rightActions = new ActionBar(this.element, { actionViewItemProvider }); - const menu = menuService.createMenu(suggestWidgetStatusbarMenu, contextKeyService); + this._leftActions = new ActionBar(this.element, { actionViewItemProvider }); + this._rightActions = new ActionBar(this.element, { actionViewItemProvider }); - leftActions.domNode.classList.add('left'); - rightActions.domNode.classList.add('right'); + this._leftActions.domNode.classList.add('left'); + this._rightActions.domNode.classList.add('right'); + } + dispose(): void { + this._menuDisposables.dispose(); + this.element.remove(); + } + + show(): void { + const menu = this._menuService.createMenu(suggestWidgetStatusbarMenu, this._contextKeyService); const renderMenu = () => { const left: IAction[] = []; const right: IAction[] = []; @@ -68,17 +75,16 @@ export class SuggestWidgetStatus { right.push(...actions); } } - leftActions.clear(); - leftActions.push(left); - rightActions.clear(); - rightActions.push(right); + this._leftActions.clear(); + this._leftActions.push(left); + this._rightActions.clear(); + this._rightActions.push(right); }; - this._disposables.add(menu.onDidChange(() => renderMenu())); - this._disposables.add(menu); + this._menuDisposables.add(menu.onDidChange(() => renderMenu())); + this._menuDisposables.add(menu); } - dispose(): void { - this._disposables.dispose(); - this.element.remove(); + hide(): void { + this._menuDisposables.clear(); } } diff --git a/src/vs/editor/contrib/suggest/test/suggestController.test.ts b/src/vs/editor/contrib/suggest/test/suggestController.test.ts index 8644d449..491a7573 100644 --- a/src/vs/editor/contrib/suggest/test/suggestController.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestController.test.ts @@ -57,6 +57,7 @@ suite('SuggestController', function () { createMenu() { return new class extends mock() { onDidChange = Event.None; + dispose() { } }; } }] diff --git a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts index 3193c678..ce99f975 100644 --- a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts @@ -71,7 +71,7 @@ suite('SuggestModel - Context', function () { this._register(TokenizationRegistry.register(this.getLanguageIdentifier().language, { getInitialState: (): IState => NULL_STATE, tokenize: undefined!, - tokenize2: (line: string, state: IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: IState): TokenizationResult2 => { const tokensArr: number[] = []; let prevLanguageId: LanguageIdentifier | undefined = undefined; for (let i = 0; i < line.length; i++) { diff --git a/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts b/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts index 44ca5839..43fdcc90 100644 --- a/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts +++ b/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts @@ -16,6 +16,7 @@ import { toMultilineTokens2, SemanticTokensProviderStyling } from 'vs/editor/com import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { isSemanticColoringEnabled, SEMANTIC_HIGHLIGHTING_SETTING_ID } from 'vs/editor/common/services/modelServiceImpl'; +import { getDocumentRangeSemanticTokensProvider } from 'vs/editor/common/services/getSemanticTokens'; class ViewportSemanticTokensContribution extends Disposable implements IEditorContribution { @@ -66,11 +67,6 @@ class ViewportSemanticTokensContribution extends Disposable implements IEditorCo })); } - private static _getSemanticColoringProvider(model: ITextModel): DocumentRangeSemanticTokensProvider | null { - const result = DocumentRangeSemanticTokensProviderRegistry.ordered(model); - return (result.length > 0 ? result[0] : null); - } - private _cancelAll(): void { for (const request of this._outstandingRequests) { request.cancel(); @@ -101,7 +97,7 @@ class ViewportSemanticTokensContribution extends Disposable implements IEditorCo } return; } - const provider = ViewportSemanticTokensContribution._getSemanticColoringProvider(model); + const provider = getDocumentRangeSemanticTokensProvider(model); if (!provider) { if (model.hasSomeSemanticTokens()) { model.setSemanticTokens(null, false); diff --git a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts index 49433e1f..ee2eb073 100644 --- a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts +++ b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts @@ -110,7 +110,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordLeft - with selection', () => { @@ -123,7 +123,7 @@ suite('WordOperations', () => { ], {}, (editor) => { editor.setPosition(new Position(5, 2)); cursorWordLeft(editor, true); - assert.deepEqual(editor.getSelection(), new Selection(5, 2, 5, 1)); + assert.deepStrictEqual(editor.getSelection(), new Selection(5, 2, 5, 1)); }); }); @@ -138,7 +138,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordLeft - issue #48046: Word selection doesn\'t work as usual', () => { @@ -154,7 +154,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordLeftSelect - issue #74369: cursorWordLeft and cursorWordLeftSelect do not behave consistently', () => { @@ -170,7 +170,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordStartLeft', () => { @@ -185,7 +185,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordStartLeft - issue #51119: regression makes VS compatibility impossible', () => { @@ -200,7 +200,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('issue #51275 - cursorWordStartLeft does not push undo/redo stack element', () => { @@ -212,16 +212,16 @@ suite('WordOperations', () => { withTestCodeEditor('', {}, (editor, viewModel) => { type(viewModel, 'foo bar baz'); - assert.equal(editor.getValue(), 'foo bar baz'); + assert.strictEqual(editor.getValue(), 'foo bar baz'); cursorWordStartLeft(editor); cursorWordStartLeft(editor); type(viewModel, 'q'); - assert.equal(editor.getValue(), 'foo qbar baz'); + assert.strictEqual(editor.getValue(), 'foo qbar baz'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(editor.getValue(), 'foo bar baz'); + assert.strictEqual(editor.getValue(), 'foo bar baz'); }); }); @@ -236,7 +236,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordRight - simple', () => { @@ -256,7 +256,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(5, 2)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordRight - selection', () => { @@ -269,7 +269,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { editor.setPosition(new Position(1, 1)); cursorWordRight(editor, true); - assert.deepEqual(editor.getSelection(), new Selection(1, 1, 1, 8)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 1, 1, 8)); }); }); @@ -286,7 +286,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 50)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordRight - issue #41199', () => { @@ -302,7 +302,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 17)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('moveWordEndRight', () => { @@ -318,7 +318,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 50)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('moveWordStartRight', () => { @@ -335,7 +335,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 50)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('issue #51119: cursorWordStartRight regression makes VS compatibility impossible', () => { @@ -350,7 +350,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 15)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('issue #64810: cursorWordStartRight skips first word after newline', () => { @@ -365,7 +365,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(2, 12)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordAccessibilityLeft', () => { @@ -379,7 +379,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordAccessibilityRight', () => { @@ -393,7 +393,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 50)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordLeft for non-empty selection', () => { @@ -407,8 +407,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setSelection(new Selection(3, 7, 3, 9)); deleteWordLeft(editor); - assert.equal(model.getLineContent(3), ' Thd Line🐶'); - assert.deepEqual(editor.getPosition(), new Position(3, 7)); + assert.strictEqual(model.getLineContent(3), ' Thd Line🐶'); + assert.deepStrictEqual(editor.getPosition(), new Position(3, 7)); }); }); @@ -423,8 +423,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 1)); deleteWordLeft(editor); - assert.equal(model.getLineContent(1), ' \tMy First Line\t '); - assert.deepEqual(editor.getPosition(), new Position(1, 1)); + assert.strictEqual(model.getLineContent(1), ' \tMy First Line\t '); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 1)); }); }); @@ -439,8 +439,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(3, 11)); deleteWordLeft(editor); - assert.equal(model.getLineContent(3), ' Line🐶'); - assert.deepEqual(editor.getPosition(), new Position(3, 5)); + assert.strictEqual(model.getLineContent(3), ' Line🐶'); + assert.deepStrictEqual(editor.getPosition(), new Position(3, 5)); }); }); @@ -455,8 +455,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(2, 11)); deleteWordLeft(editor); - assert.equal(model.getLineContent(2), '\tMy Line'); - assert.deepEqual(editor.getPosition(), new Position(2, 5)); + assert.strictEqual(model.getLineContent(2), '\tMy Line'); + assert.deepStrictEqual(editor.getPosition(), new Position(2, 5)); }); }); @@ -471,8 +471,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 12)); deleteWordLeft(editor); - assert.equal(model.getLineContent(1), ' \tMy st Line\t '); - assert.deepEqual(editor.getPosition(), new Position(1, 9)); + assert.strictEqual(model.getLineContent(1), ' \tMy st Line\t '); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 9)); }); }); @@ -487,8 +487,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setSelection(new Selection(3, 7, 3, 9)); deleteWordRight(editor); - assert.equal(model.getLineContent(3), ' Thd Line🐶'); - assert.deepEqual(editor.getPosition(), new Position(3, 7)); + assert.strictEqual(model.getLineContent(3), ' Thd Line🐶'); + assert.deepStrictEqual(editor.getPosition(), new Position(3, 7)); }); }); @@ -503,8 +503,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(5, 3)); deleteWordRight(editor); - assert.equal(model.getLineContent(5), '1'); - assert.deepEqual(editor.getPosition(), new Position(5, 2)); + assert.strictEqual(model.getLineContent(5), '1'); + assert.deepStrictEqual(editor.getPosition(), new Position(5, 2)); }); }); @@ -519,8 +519,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(3, 1)); deleteWordRight(editor); - assert.equal(model.getLineContent(3), 'Third Line🐶'); - assert.deepEqual(editor.getPosition(), new Position(3, 1)); + assert.strictEqual(model.getLineContent(3), 'Third Line🐶'); + assert.deepStrictEqual(editor.getPosition(), new Position(3, 1)); }); }); @@ -535,8 +535,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(2, 5)); deleteWordRight(editor); - assert.equal(model.getLineContent(2), '\tMy Line'); - assert.deepEqual(editor.getPosition(), new Position(2, 5)); + assert.strictEqual(model.getLineContent(2), '\tMy Line'); + assert.deepStrictEqual(editor.getPosition(), new Position(2, 5)); }); }); @@ -551,8 +551,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 11)); deleteWordRight(editor); - assert.equal(model.getLineContent(1), ' \tMy Fi Line\t '); - assert.deepEqual(editor.getPosition(), new Position(1, 11)); + assert.strictEqual(model.getLineContent(1), ' \tMy Fi Line\t '); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 11)); }); }); @@ -569,7 +569,7 @@ suite('WordOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordStartLeft', () => { @@ -585,7 +585,7 @@ suite('WordOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordEndLeft', () => { @@ -601,7 +601,7 @@ suite('WordOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordLeft - issue #24947', () => { @@ -611,7 +611,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(2, 1)); - deleteWordLeft(editor); assert.equal(model.getLineContent(1), '{}'); + deleteWordLeft(editor); assert.strictEqual(model.getLineContent(1), '{}'); }); withTestCodeEditor([ @@ -620,7 +620,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(2, 1)); - deleteWordStartLeft(editor); assert.equal(model.getLineContent(1), '{}'); + deleteWordStartLeft(editor); assert.strictEqual(model.getLineContent(1), '{}'); }); withTestCodeEditor([ @@ -629,7 +629,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(2, 1)); - deleteWordEndLeft(editor); assert.equal(model.getLineContent(1), '{}'); + deleteWordEndLeft(editor); assert.strictEqual(model.getLineContent(1), '{}'); }); }); @@ -644,7 +644,7 @@ suite('WordOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordRight - issue #3882', () => { @@ -654,7 +654,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(1, 24)); - deleteWordRight(editor); assert.equal(model.getLineContent(1), 'public void Add( int x,int y )', '001'); + deleteWordRight(editor); assert.strictEqual(model.getLineContent(1), 'public void Add( int x,int y )', '001'); }); }); @@ -665,7 +665,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(1, 24)); - deleteWordStartRight(editor); assert.equal(model.getLineContent(1), 'public void Add( int x,int y )', '001'); + deleteWordStartRight(editor); assert.strictEqual(model.getLineContent(1), 'public void Add( int x,int y )', '001'); }); }); @@ -676,7 +676,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(1, 24)); - deleteWordEndRight(editor); assert.equal(model.getLineContent(1), 'public void Add( int x,int y )', '001'); + deleteWordEndRight(editor); assert.strictEqual(model.getLineContent(1), 'public void Add( int x,int y )', '001'); }); }); @@ -691,7 +691,7 @@ suite('WordOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordEndRight', () => { @@ -705,7 +705,7 @@ suite('WordOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordRight - issue #3882 (1): Ctrl+Delete removing entire line when used at the end of line', () => { @@ -715,7 +715,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(1, 18)); - deleteWordRight(editor); assert.equal(model.getLineContent(1), 'A line with text.And another one', '001'); + deleteWordRight(editor); assert.strictEqual(model.getLineContent(1), 'A line with text.And another one', '001'); }); }); @@ -726,7 +726,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(2, 1)); - deleteWordLeft(editor); assert.equal(model.getLineContent(1), 'A line with text. And another one', '001'); + deleteWordLeft(editor); assert.strictEqual(model.getLineContent(1), 'A line with text. And another one', '001'); }); }); @@ -748,7 +748,7 @@ suite('WordOperations', () => { withTestCodeEditor(null, { model }, (editor, _) => { editor.setPosition(new Position(1, 4)); - deleteWordLeft(editor); assert.equal(model.getLineContent(1), 'a '); + deleteWordLeft(editor); assert.strictEqual(model.getLineContent(1), 'a '); }); model.dispose(); @@ -764,7 +764,7 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(2, 1)); deleteInsideWord(editor); - assert.equal(model.getValue(), 'Line1\nLine2'); + assert.strictEqual(model.getValue(), 'Line1\nLine2'); }); }); @@ -775,7 +775,7 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 6)); deleteInsideWord(editor); - assert.equal(model.getValue(), 'Justsome text.'); + assert.strictEqual(model.getValue(), 'Justsome text.'); }); }); @@ -786,7 +786,7 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 6)); deleteInsideWord(editor); - assert.equal(model.getValue(), 'Justsome text.'); + assert.strictEqual(model.getValue(), 'Justsome text.'); }); }); @@ -797,19 +797,19 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 6)); deleteInsideWord(editor); - assert.equal(model.getValue(), 'Just"some text.'); + assert.strictEqual(model.getValue(), 'Just"some text.'); deleteInsideWord(editor); - assert.equal(model.getValue(), '"some text.'); + assert.strictEqual(model.getValue(), '"some text.'); deleteInsideWord(editor); - assert.equal(model.getValue(), 'some text.'); + assert.strictEqual(model.getValue(), 'some text.'); deleteInsideWord(editor); - assert.equal(model.getValue(), 'text.'); + assert.strictEqual(model.getValue(), 'text.'); deleteInsideWord(editor); - assert.equal(model.getValue(), '.'); + assert.strictEqual(model.getValue(), '.'); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); }); }); @@ -820,19 +820,19 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 7)); deleteInsideWord(editor); - assert.equal(model.getValue(), 'x=3+45+6'); + assert.strictEqual(model.getValue(), 'x=3+45+6'); deleteInsideWord(editor); - assert.equal(model.getValue(), 'x=3++6'); + assert.strictEqual(model.getValue(), 'x=3++6'); deleteInsideWord(editor); - assert.equal(model.getValue(), 'x=36'); + assert.strictEqual(model.getValue(), 'x=36'); deleteInsideWord(editor); - assert.equal(model.getValue(), 'x='); + assert.strictEqual(model.getValue(), 'x='); deleteInsideWord(editor); - assert.equal(model.getValue(), 'x'); + assert.strictEqual(model.getValue(), 'x'); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); }); }); @@ -843,13 +843,13 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 7)); deleteInsideWord(editor); - assert.equal(model.getValue(), 'This interesting'); + assert.strictEqual(model.getValue(), 'This interesting'); deleteInsideWord(editor); - assert.equal(model.getValue(), 'This'); + assert.strictEqual(model.getValue(), 'This'); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); }); }); @@ -860,13 +860,13 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 7)); deleteInsideWord(editor); - assert.equal(model.getValue(), 'This interesting'); + assert.strictEqual(model.getValue(), 'This interesting'); deleteInsideWord(editor); - assert.equal(model.getValue(), 'This'); + assert.strictEqual(model.getValue(), 'This'); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); }); }); }); diff --git a/src/vs/editor/contrib/wordOperations/wordOperations.ts b/src/vs/editor/contrib/wordOperations/wordOperations.ts index 82dcbddc..ac3f987d 100644 --- a/src/vs/editor/contrib/wordOperations/wordOperations.ts +++ b/src/vs/editor/contrib/wordOperations/wordOperations.ts @@ -55,7 +55,7 @@ export abstract class MoveWordCommand extends EditorCommand { }); model.pushStackElement(); - editor._getViewModel().setCursorStates('moveWordCommand', CursorChangeReason.NotSet, result.map(r => CursorState.fromModelSelection(r))); + editor._getViewModel().setCursorStates('moveWordCommand', CursorChangeReason.Explicit, result.map(r => CursorState.fromModelSelection(r))); if (result.length === 1) { const pos = new Position(result[0].positionLineNumber, result[0].positionColumn); editor.revealPosition(pos, ScrollType.Smooth); diff --git a/src/vs/editor/contrib/wordPartOperations/test/wordPartOperations.test.ts b/src/vs/editor/contrib/wordPartOperations/test/wordPartOperations.test.ts index 70e9c87c..446711f2 100644 --- a/src/vs/editor/contrib/wordPartOperations/test/wordPartOperations.test.ts +++ b/src/vs/editor/contrib/wordPartOperations/test/wordPartOperations.test.ts @@ -49,7 +49,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordPartLeft - issue #53899: whitespace', () => { @@ -63,7 +63,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordPartLeft - issue #53899: underscores', () => { @@ -77,7 +77,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordPartRight - basic', () => { @@ -95,7 +95,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(3, 9)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordPartRight - issue #53899: whitespace', () => { @@ -109,7 +109,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 52)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordPartRight - issue #53899: underscores', () => { @@ -123,7 +123,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 52)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordPartRight - issue #53899: second case', () => { @@ -142,7 +142,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(4, 7)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('issue #93239 - cursorWordPartRight', () => { @@ -158,7 +158,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 8)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('issue #93239 - cursorWordPartLeft', () => { @@ -174,7 +174,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordPartLeft - basic', () => { @@ -188,7 +188,7 @@ suite('WordPartOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordPartRight - basic', () => { @@ -202,6 +202,6 @@ suite('WordPartOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); }); diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index 90428321..9b4870b4 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -23,12 +23,13 @@ import 'vs/editor/contrib/find/findController'; import 'vs/editor/contrib/folding/folding'; import 'vs/editor/contrib/fontZoom/fontZoom'; import 'vs/editor/contrib/format/formatActions'; -import 'vs/editor/contrib/gotoSymbol/documentSymbols'; +import 'vs/editor/contrib/documentSymbols/documentSymbols'; import 'vs/editor/contrib/gotoSymbol/goToCommands'; import 'vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition'; import 'vs/editor/contrib/gotoError/gotoError'; import 'vs/editor/contrib/hover/hover'; import 'vs/editor/contrib/indentation/indentation'; +import 'vs/editor/contrib/inlineHints/inlineHintsController'; import 'vs/editor/contrib/inPlaceReplace/inPlaceReplace'; import 'vs/editor/contrib/linesOperations/linesOperations'; import 'vs/editor/contrib/linkedEditing/linkedEditing'; diff --git a/src/vs/editor/editor.api.ts b/src/vs/editor/editor.api.ts index 0e4c48c3..6b2cc6c0 100644 --- a/src/vs/editor/editor.api.ts +++ b/src/vs/editor/editor.api.ts @@ -7,6 +7,8 @@ import { EditorOptions, WrappingIndent, EditorAutoIndentStrategy } from 'vs/edit import { createMonacoBaseAPI } from 'vs/editor/common/standalone/standaloneBase'; import { createMonacoEditorAPI } from 'vs/editor/standalone/browser/standaloneEditor'; import { createMonacoLanguagesAPI } from 'vs/editor/standalone/browser/standaloneLanguages'; +import { globals } from 'vs/base/common/platform'; +import { FormattingConflicts } from 'vs/editor/contrib/format/format'; // Set defaults for standalone editor EditorOptions.wrappingIndent.defaultValue = WrappingIndent.None; @@ -14,6 +16,10 @@ EditorOptions.glyphMargin.defaultValue = false; EditorOptions.autoIndent.defaultValue = EditorAutoIndentStrategy.Advanced; EditorOptions.overviewRulerLanes.defaultValue = 2; +// We need to register a formatter selector which simply picks the first available formatter. +// See https://github.com/microsoft/monaco-editor/issues/2327 +FormattingConflicts.setFormatterSelector((formatter, document, mode) => Promise.resolve(formatter[0])); + const api = createMonacoBaseAPI(); api.editor = createMonacoEditorAPI(); api.languages = createMonacoLanguagesAPI(); @@ -32,7 +38,9 @@ export const Token = api.Token; export const editor = api.editor; export const languages = api.languages; -self.monaco = api; +if (globals.MonacoEnvironment?.globalAPI || globals.define?.amd) { + self.monaco = api; +} if (typeof self.require !== 'undefined' && typeof self.require.config === 'function') { self.require.config({ diff --git a/src/vs/editor/standalone/browser/colorizer.ts b/src/vs/editor/standalone/browser/colorizer.ts index 22bad13e..f9d33569 100644 --- a/src/vs/editor/standalone/browser/colorizer.ts +++ b/src/vs/editor/standalone/browser/colorizer.ts @@ -42,8 +42,8 @@ export class Colorizer { let text = domNode.firstChild ? domNode.firstChild.nodeValue : ''; domNode.className += ' ' + theme; let render = (str: string) => { - const trustedhtml = ttPolicy ? ttPolicy.createHTML(str) : str; - domNode.innerHTML = trustedhtml as unknown as string; + const trustedhtml = ttPolicy?.createHTML(str) ?? str; + domNode.innerHTML = trustedhtml as string; }; return this.colorize(modeService, text || '', mimeType, options).then(render, (err) => console.error(err)); } @@ -222,7 +222,7 @@ function _actualColorize(lines: string[], tabSize: number, tokenizationSupport: for (let i = 0, length = lines.length; i < length; i++) { let line = lines[i]; - let tokenizeResult = tokenizationSupport.tokenize2(line, state, 0); + let tokenizeResult = tokenizationSupport.tokenize2(line, true, state, 0); LineTokens.convertToEndOffset(tokenizeResult.tokens, line.length); let lineTokens = new LineTokens(tokenizeResult.tokens, line); const isBasicASCII = ViewLineRenderingData.isBasicASCII(line, /* check for basic ASCII */true); diff --git a/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts b/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts index 192f9218..88b54c86 100644 --- a/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts +++ b/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts @@ -137,8 +137,8 @@ function getSafeTokenizationSupport(languageIdentifier: LanguageIdentifier): ITo } return { getInitialState: () => NULL_STATE, - tokenize: (line: string, state: IState, deltaOffset: number) => nullTokenize(languageIdentifier.language, line, state, deltaOffset), - tokenize2: (line: string, state: IState, deltaOffset: number) => nullTokenize2(languageIdentifier.id, line, state, deltaOffset) + tokenize: (line: string, hasEOL: boolean, state: IState, deltaOffset: number) => nullTokenize(languageIdentifier.language, line, state, deltaOffset), + tokenize2: (line: string, hasEOL: boolean, state: IState, deltaOffset: number) => nullTokenize2(languageIdentifier.id, line, state, deltaOffset) }; } @@ -293,8 +293,8 @@ class InspectTokensWidget extends Disposable implements IContentWidget { private _getTokensAtLine(lineNumber: number): ICompleteLineTokenization { let stateBeforeLine = this._getStateBeforeLine(lineNumber); - let tokenizationResult1 = this._tokenizationSupport.tokenize(this._model.getLineContent(lineNumber), stateBeforeLine, 0); - let tokenizationResult2 = this._tokenizationSupport.tokenize2(this._model.getLineContent(lineNumber), stateBeforeLine, 0); + let tokenizationResult1 = this._tokenizationSupport.tokenize(this._model.getLineContent(lineNumber), true, stateBeforeLine, 0); + let tokenizationResult2 = this._tokenizationSupport.tokenize2(this._model.getLineContent(lineNumber), true, stateBeforeLine, 0); return { startState: stateBeforeLine, @@ -308,7 +308,7 @@ class InspectTokensWidget extends Disposable implements IContentWidget { let state: IState = this._tokenizationSupport.getInitialState(); for (let i = 1; i < lineNumber; i++) { - let tokenizationResult = this._tokenizationSupport.tokenize(this._model.getLineContent(i), state, 0); + let tokenizationResult = this._tokenizationSupport.tokenize(this._model.getLineContent(i), true, state, 0); state = tokenizationResult.endState; } diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index ba51dfd9..3410b586 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -176,8 +176,8 @@ export class SimpleEditorProgressService implements IEditorProgressService { return SimpleEditorProgressService.NULL_PROGRESS_RUNNER; } - showWhile(promise: Promise, delay?: number): Promise { - return Promise.resolve(undefined); + async showWhile(promise: Promise, delay?: number): Promise { + await promise; } } @@ -638,12 +638,12 @@ export class SimpleWorkspaceContextService implements IWorkspaceContextService { return resource && resource.scheme === SimpleWorkspaceContextService.SCHEME; } - public isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean { + public isCurrentWorkspace(workspaceIdOrFolder: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI): boolean { return true; } } -export function applyConfigurationValues(configurationService: IConfigurationService, source: any, isDiffEditor: boolean): void { +export function updateConfigurationService(configurationService: IConfigurationService, source: any, isDiffEditor: boolean): void { if (!source) { return; } @@ -736,7 +736,7 @@ export class SimpleUriLabelService implements ILabelService { return basename(resource); } - public getWorkspaceLabel(workspace: IWorkspaceIdentifier | URI | IWorkspace, options?: { verbose: boolean; }): string { + public getWorkspaceLabel(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | IWorkspace, options?: { verbose: boolean; }): string { return ''; } diff --git a/src/vs/editor/standalone/browser/standalone-tokens.css b/src/vs/editor/standalone/browser/standalone-tokens.css index e97572e0..17427300 100644 --- a/src/vs/editor/standalone/browser/standalone-tokens.css +++ b/src/vs/editor/standalone/browser/standalone-tokens.css @@ -23,6 +23,19 @@ margin: 0; } +/* +In certain cases, the default positioning of the aria container (left: -999em) can cause scrollbars to appear. +So here we try to avoid that by using a different technique. See https://stackoverflow.com/a/26032207 +*/ +.monaco-aria-container { + position: absolute !important; + height: 1px; + width: 1px; + left: inherit !important; + overflow: hidden; + clip: rect(1px, 1px, 1px, 1px); +} + /* The hc-black theme is already high contrast optimized */ .monaco-editor.hc-black { -ms-high-contrast-adjust: none; diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index 9344ee80..4e3c939d 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -14,7 +14,7 @@ import { InternalEditorAction } from 'vs/editor/common/editorAction'; import { IModelChangedEvent } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; -import { StandaloneKeybindingService, applyConfigurationValues } from 'vs/editor/standalone/browser/simpleServices'; +import { StandaloneKeybindingService, updateConfigurationService } from 'vs/editor/standalone/browser/simpleServices'; import { IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneThemeService'; import { IMenuItem, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { CommandsRegistry, ICommandHandler, ICommandService } from 'vs/platform/commands/common/commands'; @@ -31,6 +31,9 @@ import { StandaloneCodeEditorNLS } from 'vs/editor/common/standaloneStrings'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { StandaloneThemeServiceImpl } from 'vs/editor/standalone/browser/standaloneThemeServiceImpl'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { ILanguageSelection, IModeService } from 'vs/editor/common/services/modeService'; +import { URI } from 'vs/base/common/uri'; /** * Description of an action contribution @@ -227,7 +230,7 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon constructor( domElement: HTMLElement, - options: IStandaloneEditorConstructionOptions, + _options: Readonly, @IInstantiationService instantiationService: IInstantiationService, @ICodeEditorService codeEditorService: ICodeEditorService, @ICommandService commandService: ICommandService, @@ -237,7 +240,7 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon @INotificationService notificationService: INotificationService, @IAccessibilityService accessibilityService: IAccessibilityService ) { - options = options || {}; + const options = { ..._options }; options.ariaLabel = options.ariaLabel || StandaloneCodeEditorNLS.editorViewAccessibleLabel; options.ariaLabel = options.ariaLabel + ';' + (StandaloneCodeEditorNLS.accessibilityHelpMessage); super(domElement, options, {}, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService); @@ -353,7 +356,7 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon constructor( domElement: HTMLElement, - options: IStandaloneEditorConstructionOptions | undefined, + _options: Readonly | undefined, toDispose: IDisposable, @IInstantiationService instantiationService: IInstantiationService, @ICodeEditorService codeEditorService: ICodeEditorService, @@ -364,11 +367,13 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon @IStandaloneThemeService themeService: IStandaloneThemeService, @INotificationService notificationService: INotificationService, @IConfigurationService configurationService: IConfigurationService, - @IAccessibilityService accessibilityService: IAccessibilityService + @IAccessibilityService accessibilityService: IAccessibilityService, + @IModelService modelService: IModelService, + @IModeService modeService: IModeService, ) { - applyConfigurationValues(configurationService, options, false); + const options = { ..._options }; + updateConfigurationService(configurationService, options, false); const themeDomRegistration = (themeService).registerEditorContainer(domElement); - options = options || {}; if (typeof options.theme === 'string') { themeService.setTheme(options.theme); } @@ -384,7 +389,7 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon let model: ITextModel | null; if (typeof _model === 'undefined') { - model = (self).monaco.editor.createModel(options.value || '', options.language || 'text/plain'); + model = createTextModel(modelService, modeService, options.value || '', options.language || 'text/plain', undefined); this._ownsModel = true; } else { model = _model; @@ -405,8 +410,8 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon super.dispose(); } - public updateOptions(newOptions: IEditorOptions & IGlobalEditorOptions): void { - applyConfigurationValues(this._configurationService, newOptions, false); + public updateOptions(newOptions: Readonly): void { + updateConfigurationService(this._configurationService, newOptions, false); if (typeof newOptions.theme === 'string') { this._standaloneThemeService.setTheme(newOptions.theme); } @@ -437,7 +442,7 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon constructor( domElement: HTMLElement, - options: IDiffEditorConstructionOptions | undefined, + _options: Readonly | undefined, toDispose: IDisposable, @IInstantiationService instantiationService: IInstantiationService, @IContextKeyService contextKeyService: IContextKeyService, @@ -452,14 +457,14 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon @IEditorProgressService editorProgressService: IEditorProgressService, @IClipboardService clipboardService: IClipboardService, ) { - applyConfigurationValues(configurationService, options, true); + const options = { ..._options }; + updateConfigurationService(configurationService, options, true); const themeDomRegistration = (themeService).registerEditorContainer(domElement); - options = options || {}; if (typeof options.theme === 'string') { options.theme = themeService.setTheme(options.theme); } - super(domElement, options, clipboardService, editorWorkerService, contextKeyService, instantiationService, codeEditorService, themeService, notificationService, contextMenuService, editorProgressService); + super(domElement, options, {}, clipboardService, editorWorkerService, contextKeyService, instantiationService, codeEditorService, themeService, notificationService, contextMenuService, editorProgressService); this._contextViewService = contextViewService; this._configurationService = configurationService; @@ -475,15 +480,15 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon super.dispose(); } - public updateOptions(newOptions: IDiffEditorOptions & IGlobalEditorOptions): void { - applyConfigurationValues(this._configurationService, newOptions, true); + public updateOptions(newOptions: Readonly): void { + updateConfigurationService(this._configurationService, newOptions, true); if (typeof newOptions.theme === 'string') { this._standaloneThemeService.setTheme(newOptions.theme); } super.updateOptions(newOptions); } - protected _createInnerEditor(instantiationService: IInstantiationService, container: HTMLElement, options: IEditorOptions): CodeEditorWidget { + protected _createInnerEditor(instantiationService: IInstantiationService, container: HTMLElement, options: Readonly): CodeEditorWidget { return instantiationService.createInstance(StandaloneCodeEditor, container, options); } @@ -507,3 +512,26 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon return this.getModifiedEditor().addAction(descriptor); } } + +/** + * @internal + */ +export function createTextModel(modelService: IModelService, modeService: IModeService, value: string, language: string | undefined, uri: URI | undefined): ITextModel { + value = value || ''; + if (!language) { + const firstLF = value.indexOf('\n'); + let firstLine = value; + if (firstLF !== -1) { + firstLine = value.substring(0, firstLF); + } + return doCreateModel(modelService, value, modeService.createByFilepathOrFirstLine(uri || null, firstLine), uri); + } + return doCreateModel(modelService, value, modeService.create(language), uri); +} + +/** + * @internal + */ +function doCreateModel(modelService: IModelService, value: string, languageSelection: ILanguageSelection, uri: URI | undefined): ITextModel { + return modelService.createModel(value, languageSelection, uri); +} diff --git a/src/vs/editor/standalone/browser/standaloneEditor.ts b/src/vs/editor/standalone/browser/standaloneEditor.ts index 6d5fc9c3..295f2cb6 100644 --- a/src/vs/editor/standalone/browser/standaloneEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneEditor.ts @@ -18,16 +18,16 @@ import { FindMatch, ITextModel, TextModelResolvedOptions } from 'vs/editor/commo import * as modes from 'vs/editor/common/modes'; import { NULL_STATE, nullTokenize } from 'vs/editor/common/modes/nullMode'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; -import { ILanguageSelection } from 'vs/editor/common/services/modeService'; +import { IModeService } from 'vs/editor/common/services/modeService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IWebWorkerOptions, MonacoWebWorker, createWebWorker as actualCreateWebWorker } from 'vs/editor/common/services/webWorker'; import * as standaloneEnums from 'vs/editor/common/standalone/standaloneEnums'; import { Colorizer, IColorizerElementOptions, IColorizerOptions } from 'vs/editor/standalone/browser/colorizer'; import { SimpleEditorModelResolverService } from 'vs/editor/standalone/browser/simpleServices'; -import { IDiffEditorConstructionOptions, IStandaloneEditorConstructionOptions, IStandaloneCodeEditor, IStandaloneDiffEditor, StandaloneDiffEditor, StandaloneEditor } from 'vs/editor/standalone/browser/standaloneCodeEditor'; +import { IDiffEditorConstructionOptions, IStandaloneEditorConstructionOptions, IStandaloneCodeEditor, IStandaloneDiffEditor, StandaloneDiffEditor, StandaloneEditor, createTextModel } from 'vs/editor/standalone/browser/standaloneCodeEditor'; import { DynamicStandaloneServices, IEditorOverrideServices, StaticServices } from 'vs/editor/standalone/browser/standaloneServices'; import { IStandaloneThemeData, IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneThemeService'; -import { ICommandService } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -42,6 +42,7 @@ import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { StandaloneThemeServiceImpl } from 'vs/editor/standalone/browser/standaloneThemeServiceImpl'; import { splitLines } from 'vs/base/common/strings'; +import { IModelService } from 'vs/editor/common/services/modelService'; type Omit = Pick>; @@ -87,7 +88,9 @@ export function create(domElement: HTMLElement, options?: IStandaloneEditorConst services.get(IStandaloneThemeService), services.get(INotificationService), services.get(IConfigurationService), - services.get(IAccessibilityService) + services.get(IAccessibilityService), + services.get(IModelService), + services.get(IModeService), ); }); } @@ -140,27 +143,18 @@ export function createDiffNavigator(diffEditor: IStandaloneDiffEditor, opts?: ID return new DiffNavigator(diffEditor, opts); } -function doCreateModel(value: string, languageSelection: ILanguageSelection, uri?: URI): ITextModel { - return StaticServices.modelService.get().createModel(value, languageSelection, uri); -} - /** * Create a new editor model. * You can specify the language that should be set for this model or let the language be inferred from the `uri`. */ export function createModel(value: string, language?: string, uri?: URI): ITextModel { - value = value || ''; - - if (!language) { - let firstLF = value.indexOf('\n'); - let firstLine = value; - if (firstLF !== -1) { - firstLine = value.substring(0, firstLF); - } - - return doCreateModel(value, StaticServices.modeService.get().createByFilepathOrFirstLine(uri || null, firstLine), uri); - } - return doCreateModel(value, StaticServices.modeService.get().create(language), uri); + return createTextModel( + StaticServices.modelService.get(), + StaticServices.modeService.get(), + value, + language, + uri + ); } /** @@ -188,6 +182,14 @@ export function getModelMarkers(filter: { owner?: string, resource?: URI, take?: return StaticServices.markerService.get().read(filter); } +/** + * Emitted when markers change for a model. + * @event + */ +export function onDidChangeMarkers(listener: (e: readonly URI[]) => void): IDisposable { + return StaticServices.markerService.get().onMarkerChanged(listener); +} + /** * Get the model that has `uri` if it exists. */ @@ -276,7 +278,7 @@ function getSafeTokenizationSupport(language: string): Omit NULL_STATE, - tokenize: (line: string, state: modes.IState, deltaOffset: number) => nullTokenize(language, line, state, deltaOffset) + tokenize: (line: string, hasEOL: boolean, state: modes.IState, deltaOffset: number) => nullTokenize(language, line, state, deltaOffset) }; } @@ -294,7 +296,7 @@ export function tokenize(text: string, languageId: string): Token[][] { let state = tokenizationSupport.getInitialState(); for (let i = 0, len = lines.length; i < len; i++) { let line = lines[i]; - let tokenizationResult = tokenizationSupport.tokenize(line, state, 0); + let tokenizationResult = tokenizationSupport.tokenize(line, true, state, 0); result[i] = tokenizationResult.tokens; state = tokenizationResult.endState; @@ -323,6 +325,13 @@ export function remeasureFonts(): void { clearAllFontInfos(); } +/** + * Register a command. + */ +export function registerCommand(id: string, handler: (accessor: any, ...args: any[]) => void): IDisposable { + return CommandsRegistry.registerCommand({ id, handler }); +} + /** * @internal */ @@ -338,6 +347,7 @@ export function createMonacoEditorAPI(): typeof monaco.editor { setModelLanguage: setModelLanguage, setModelMarkers: setModelMarkers, getModelMarkers: getModelMarkers, + onDidChangeMarkers: onDidChangeMarkers, getModels: getModels, getModel: getModel, onDidCreateModel: onDidCreateModel, @@ -353,6 +363,7 @@ export function createMonacoEditorAPI(): typeof monaco.editor { defineTheme: defineTheme, setTheme: setTheme, remeasureFonts: remeasureFonts, + registerCommand: registerCommand, // enums AccessibilitySupport: standaloneEnums.AccessibilitySupport, diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index dbf225a6..b75a8144 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; +import { Color } from 'vs/base/common/color'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -87,14 +88,14 @@ export class EncodedTokenizationSupport2Adapter implements modes.ITokenizationSu return this._actual.getInitialState(); } - public tokenize(line: string, state: modes.IState, offsetDelta: number): TokenizationResult { + public tokenize(line: string, hasEOL: boolean, state: modes.IState, offsetDelta: number): TokenizationResult { if (typeof this._actual.tokenize === 'function') { return TokenizationSupport2Adapter.adaptTokenize(this._languageIdentifier.language, <{ tokenize(line: string, state: modes.IState): ILineTokens; }>this._actual, line, state, offsetDelta); } throw new Error('Not supported!'); } - public tokenize2(line: string, state: modes.IState): TokenizationResult2 { + public tokenize2(line: string, hasEOL: boolean, state: modes.IState): TokenizationResult2 { let result = this._actual.tokenizeEncoded(line, state); return new TokenizationResult2(result.tokens, result.endState); } @@ -157,7 +158,7 @@ export class TokenizationSupport2Adapter implements modes.ITokenizationSupport { return new TokenizationResult(tokens, endState); } - public tokenize(line: string, state: modes.IState, offsetDelta: number): TokenizationResult { + public tokenize(line: string, hasEOL: boolean, state: modes.IState, offsetDelta: number): TokenizationResult { return TokenizationSupport2Adapter.adaptTokenize(this._languageIdentifier.language, this._actual, line, state, offsetDelta); } @@ -199,7 +200,7 @@ export class TokenizationSupport2Adapter implements modes.ITokenizationSupport { return actualResult; } - public tokenize2(line: string, state: modes.IState, offsetDelta: number): TokenizationResult2 { + public tokenize2(line: string, hasEOL: boolean, state: modes.IState, offsetDelta: number): TokenizationResult2 { let actualResult = this._actual.tokenize(line, state); let tokens = this._toBinaryTokens(actualResult.tokens, offsetDelta); @@ -310,6 +311,22 @@ function isThenable(obj: any): obj is Thenable { return obj && typeof obj.then === 'function'; } +/** + * Change the color map that is used for token colors. + * Supported formats (hex): #RRGGBB, $RRGGBBAA, #RGB, #RGBA + */ +export function setColorMap(colorMap: string[] | null): void { + if (colorMap) { + const result: Color[] = [null!]; + for (let i = 1, len = colorMap.length; i < len; i++) { + result[i] = Color.fromHex(colorMap[i]); + } + StaticServices.standaloneThemeService.get().setColorMapOverride(result); + } else { + StaticServices.standaloneThemeService.get().setColorMapOverride(null); + } +} + /** * Set the tokens provider for a language (manual implementation). */ @@ -570,6 +587,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { // provider methods setLanguageConfiguration: setLanguageConfiguration, + setColorMap: setColorMap, setTokensProvider: setTokensProvider, setMonarchTokensProvider: setMonarchTokensProvider, registerReferenceProvider: registerReferenceProvider, diff --git a/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts b/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts index 544d7b07..f61c9de8 100644 --- a/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts +++ b/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts @@ -39,7 +39,11 @@ class StandaloneTheme implements IStandaloneTheme { this.themeData = standaloneThemeData; let base = standaloneThemeData.base; if (name.length > 0) { - this.id = base + ' ' + name; + if (isBuiltinTheme(name)) { + this.id = name; + } else { + this.id = base + ' ' + name; + } this.themeName = name; } else { this.id = base; @@ -199,6 +203,7 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon private _allCSS: string; private _globalStyleElement: HTMLStyleElement | null; private _styleElements: HTMLStyleElement[]; + private _colorMapOverride: Color[] | null; private _theme!: IStandaloneTheme; constructor() { @@ -216,6 +221,7 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon this._allCSS = `${this._codiconCSS}\n${this._themeCSS}`; this._globalStyleElement = null; this._styleElements = []; + this._colorMapOverride = null; this.setTheme(VS_THEME_NAME); iconRegistry.onDidChange(() => { @@ -284,6 +290,11 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon return this._theme; } + public setColorMapOverride(colorMapOverride: Color[] | null): void { + this._colorMapOverride = colorMapOverride; + this._updateThemeOrColorMap(); + } + public setTheme(themeName: string): string { let theme: StandaloneTheme; if (this._knownThemes.has(themeName)) { @@ -296,7 +307,11 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon return theme.id; } this._theme = theme; + this._updateThemeOrColorMap(); + return theme.id; + } + private _updateThemeOrColorMap(): void { let cssRules: string[] = []; let hasRule: { [rule: string]: boolean; } = {}; let ruleCollector: ICssStyleCollector = { @@ -307,19 +322,16 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon } } }; - themingRegistry.getThemingParticipants().forEach(p => p(theme, ruleCollector, this._environment)); + themingRegistry.getThemingParticipants().forEach(p => p(this._theme, ruleCollector, this._environment)); - let tokenTheme = theme.tokenTheme; - let colorMap = tokenTheme.getColorMap(); + const colorMap = this._colorMapOverride || this._theme.tokenTheme.getColorMap(); ruleCollector.addRule(generateTokensCSSForColorMap(colorMap)); this._themeCSS = cssRules.join('\n'); this._updateCSS(); TokenizationRegistry.setColorMap(colorMap); - this._onColorThemeChange.fire(theme); - - return theme.id; + this._onColorThemeChange.fire(this._theme); } private _updateCSS(): void { diff --git a/src/vs/editor/standalone/common/monarch/monarchCommon.ts b/src/vs/editor/standalone/common/monarch/monarchCommon.ts index 6dbf14dc..bd96a9f0 100644 --- a/src/vs/editor/standalone/common/monarch/monarchCommon.ts +++ b/src/vs/editor/standalone/common/monarch/monarchCommon.ts @@ -22,6 +22,7 @@ export const enum MonarchBracket { export interface ILexerMin { languageId: string; + includeLF: boolean; noThrow: boolean; ignoreCase: boolean; unicode: boolean; diff --git a/src/vs/editor/standalone/common/monarch/monarchCompile.ts b/src/vs/editor/standalone/common/monarch/monarchCompile.ts index 289d045a..aa487e7e 100644 --- a/src/vs/editor/standalone/common/monarch/monarchCompile.ts +++ b/src/vs/editor/standalone/common/monarch/monarchCompile.ts @@ -395,6 +395,7 @@ export function compile(languageId: string, json: IMonarchLanguage): monarchComm // Create our lexer let lexer: monarchCommon.ILexer = {}; lexer.languageId = languageId; + lexer.includeLF = bool(json.includeLF, false); lexer.noThrow = false; // raise exceptions during compilation lexer.maxStack = 100; @@ -411,6 +412,7 @@ export function compile(languageId: string, json: IMonarchLanguage): monarchComm // For calling compileAction later on let lexerMin: monarchCommon.ILexerMin = json; lexerMin.languageId = languageId; + lexerMin.includeLF = lexer.includeLF; lexerMin.ignoreCase = lexer.ignoreCase; lexerMin.unicode = lexer.unicode; lexerMin.noThrow = lexer.noThrow; diff --git a/src/vs/editor/standalone/common/monarch/monarchLexer.ts b/src/vs/editor/standalone/common/monarch/monarchLexer.ts index e566f758..dc8b8644 100644 --- a/src/vs/editor/standalone/common/monarch/monarchLexer.ts +++ b/src/vs/editor/standalone/common/monarch/monarchLexer.ts @@ -231,7 +231,7 @@ class MonarchLineState implements modes.IState { interface IMonarchTokensCollector { enterMode(startOffset: number, modeId: string): void; emit(startOffset: number, type: string): void; - nestedModeTokenize(embeddedModeLine: string, embeddedModeData: EmbeddedModeData, offsetDelta: number): modes.IState; + nestedModeTokenize(embeddedModeLine: string, hasEOL: boolean, embeddedModeData: EmbeddedModeData, offsetDelta: number): modes.IState; } class MonarchClassicTokensCollector implements IMonarchTokensCollector { @@ -261,7 +261,7 @@ class MonarchClassicTokensCollector implements IMonarchTokensCollector { this._tokens.push(new Token(startOffset, type, this._language!)); } - public nestedModeTokenize(embeddedModeLine: string, embeddedModeData: EmbeddedModeData, offsetDelta: number): modes.IState { + public nestedModeTokenize(embeddedModeLine: string, hasEOL: boolean, embeddedModeData: EmbeddedModeData, offsetDelta: number): modes.IState { const nestedModeId = embeddedModeData.modeId; const embeddedModeState = embeddedModeData.state; @@ -272,7 +272,7 @@ class MonarchClassicTokensCollector implements IMonarchTokensCollector { return embeddedModeState; } - let nestedResult = nestedModeTokenizationSupport.tokenize(embeddedModeLine, embeddedModeState, offsetDelta); + let nestedResult = nestedModeTokenizationSupport.tokenize(embeddedModeLine, hasEOL, embeddedModeState, offsetDelta); this._tokens = this._tokens.concat(nestedResult.tokens); this._lastTokenType = null; this._lastTokenLanguage = null; @@ -345,7 +345,7 @@ class MonarchModernTokensCollector implements IMonarchTokensCollector { return result; } - public nestedModeTokenize(embeddedModeLine: string, embeddedModeData: EmbeddedModeData, offsetDelta: number): modes.IState { + public nestedModeTokenize(embeddedModeLine: string, hasEOL: boolean, embeddedModeData: EmbeddedModeData, offsetDelta: number): modes.IState { const nestedModeId = embeddedModeData.modeId; const embeddedModeState = embeddedModeData.state; @@ -356,7 +356,7 @@ class MonarchModernTokensCollector implements IMonarchTokensCollector { return embeddedModeState; } - let nestedResult = nestedModeTokenizationSupport.tokenize2(embeddedModeLine, embeddedModeState, offsetDelta); + let nestedResult = nestedModeTokenizationSupport.tokenize2(embeddedModeLine, hasEOL, embeddedModeState, offsetDelta); this._prependTokens = MonarchModernTokensCollector._merge(this._prependTokens, this._tokens, nestedResult.tokens); this._tokens = []; this._currentLanguageId = 0; @@ -456,23 +456,23 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { return MonarchLineStateFactory.create(rootState, null); } - public tokenize(line: string, lineState: modes.IState, offsetDelta: number): TokenizationResult { + public tokenize(line: string, hasEOL: boolean, lineState: modes.IState, offsetDelta: number): TokenizationResult { let tokensCollector = new MonarchClassicTokensCollector(); - let endLineState = this._tokenize(line, lineState, offsetDelta, tokensCollector); + let endLineState = this._tokenize(line, hasEOL, lineState, offsetDelta, tokensCollector); return tokensCollector.finalize(endLineState); } - public tokenize2(line: string, lineState: modes.IState, offsetDelta: number): TokenizationResult2 { + public tokenize2(line: string, hasEOL: boolean, lineState: modes.IState, offsetDelta: number): TokenizationResult2 { let tokensCollector = new MonarchModernTokensCollector(this._modeService, this._standaloneThemeService.getColorTheme().tokenTheme); - let endLineState = this._tokenize(line, lineState, offsetDelta, tokensCollector); + let endLineState = this._tokenize(line, hasEOL, lineState, offsetDelta, tokensCollector); return tokensCollector.finalize(endLineState); } - private _tokenize(line: string, lineState: MonarchLineState, offsetDelta: number, collector: IMonarchTokensCollector): MonarchLineState { + private _tokenize(line: string, hasEOL: boolean, lineState: MonarchLineState, offsetDelta: number, collector: IMonarchTokensCollector): MonarchLineState { if (lineState.embeddedModeData) { - return this._nestedTokenize(line, lineState, offsetDelta, collector); + return this._nestedTokenize(line, hasEOL, lineState, offsetDelta, collector); } else { - return this._myTokenize(line, lineState, offsetDelta, collector); + return this._myTokenize(line, hasEOL, lineState, offsetDelta, collector); } } @@ -518,24 +518,24 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { return popOffset; } - private _nestedTokenize(line: string, lineState: MonarchLineState, offsetDelta: number, tokensCollector: IMonarchTokensCollector): MonarchLineState { + private _nestedTokenize(line: string, hasEOL: boolean, lineState: MonarchLineState, offsetDelta: number, tokensCollector: IMonarchTokensCollector): MonarchLineState { let popOffset = this._findLeavingNestedModeOffset(line, lineState); if (popOffset === -1) { // tokenization will not leave nested mode - let nestedEndState = tokensCollector.nestedModeTokenize(line, lineState.embeddedModeData!, offsetDelta); + let nestedEndState = tokensCollector.nestedModeTokenize(line, hasEOL, lineState.embeddedModeData!, offsetDelta); return MonarchLineStateFactory.create(lineState.stack, new EmbeddedModeData(lineState.embeddedModeData!.modeId, nestedEndState)); } let nestedModeLine = line.substring(0, popOffset); if (nestedModeLine.length > 0) { // tokenize with the nested mode - tokensCollector.nestedModeTokenize(nestedModeLine, lineState.embeddedModeData!, offsetDelta); + tokensCollector.nestedModeTokenize(nestedModeLine, false, lineState.embeddedModeData!, offsetDelta); } let restOfTheLine = line.substring(popOffset); - return this._myTokenize(restOfTheLine, lineState, offsetDelta + popOffset, tokensCollector); + return this._myTokenize(restOfTheLine, hasEOL, lineState, offsetDelta + popOffset, tokensCollector); } private _safeRuleName(rule: monarchCommon.IRule | null): string { @@ -545,9 +545,11 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { return '(unknown)'; } - private _myTokenize(line: string, lineState: MonarchLineState, offsetDelta: number, tokensCollector: IMonarchTokensCollector): MonarchLineState { + private _myTokenize(lineWithoutLF: string, hasEOL: boolean, lineState: MonarchLineState, offsetDelta: number, tokensCollector: IMonarchTokensCollector): MonarchLineState { tokensCollector.enterMode(offsetDelta, this._modeId); + const lineWithoutLFLength = lineWithoutLF.length; + const line = (hasEOL && this._lexer.includeLF ? lineWithoutLF + '\n' : lineWithoutLF); const lineLength = line.length; let embeddedModeData = lineState.embeddedModeData; @@ -563,7 +565,7 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { } let groupMatching: GroupMatching | null = null; - // See https://github.com/microsoft/monaco-editor/issues/1235: + // See https://github.com/microsoft/monaco-editor/issues/1235 // Evaluate rules at least once for an empty line let forceEvaluation = true; @@ -752,8 +754,8 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { if (pos < lineLength) { // there is content from the embedded mode on this line - const restOfLine = line.substr(pos); - return this._nestedTokenize(restOfLine, MonarchLineStateFactory.create(stack, embeddedModeData), offsetDelta + pos, tokensCollector); + const restOfLine = lineWithoutLF.substr(pos); + return this._nestedTokenize(restOfLine, hasEOL, MonarchLineStateFactory.create(stack, embeddedModeData), offsetDelta + pos, tokensCollector); } else { return MonarchLineStateFactory.create(stack, embeddedModeData); } @@ -831,7 +833,9 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { tokenType = monarchCommon.sanitize(token); } - tokensCollector.emit(pos0 + offsetDelta, tokenType); + if (pos0 < lineWithoutLFLength) { + tokensCollector.emit(pos0 + offsetDelta, tokenType); + } } if (enteringEmbeddedMode !== null) { diff --git a/src/vs/editor/standalone/common/monarch/monarchTypes.ts b/src/vs/editor/standalone/common/monarch/monarchTypes.ts index 19936be8..5e3a798c 100644 --- a/src/vs/editor/standalone/common/monarch/monarchTypes.ts +++ b/src/vs/editor/standalone/common/monarch/monarchTypes.ts @@ -41,6 +41,11 @@ export interface IMonarchLanguage { * attach this to every token class (by default '.' + name) */ tokenPostfix?: string; + /** + * include line feeds (in the form of a \n character) at the end of lines + * Defaults to false + */ + includeLF?: boolean; } /** diff --git a/src/vs/editor/standalone/common/standaloneThemeService.ts b/src/vs/editor/standalone/common/standaloneThemeService.ts index c70a713d..afefd369 100644 --- a/src/vs/editor/standalone/common/standaloneThemeService.ts +++ b/src/vs/editor/standalone/common/standaloneThemeService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Color } from 'vs/base/common/color'; import { ITokenThemeRule, TokenTheme } from 'vs/editor/common/modes/supports/tokenization'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; @@ -33,4 +34,7 @@ export interface IStandaloneThemeService extends IThemeService { defineTheme(themeName: string, themeData: IStandaloneThemeData): void; getColorTheme(): IStandaloneTheme; + + setColorMapOverride(colorMapOverride: Color[] | null): void; + } diff --git a/src/vs/editor/standalone/common/themes.ts b/src/vs/editor/standalone/common/themes.ts index 4c7761e6..9e07df46 100644 --- a/src/vs/editor/standalone/common/themes.ts +++ b/src/vs/editor/standalone/common/themes.ts @@ -65,7 +65,7 @@ export const vs: IStandaloneThemeData = { { token: 'operator.scss', foreground: '666666' }, { token: 'operator.sql', foreground: '778899' }, { token: 'operator.swift', foreground: '666666' }, - { token: 'predefined.sql', foreground: 'FF00FF' }, + { token: 'predefined.sql', foreground: 'C700C7' }, ], colors: { [editorBackground]: '#FFFFFE', diff --git a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts index e3c7a31f..bd4399d6 100644 --- a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts +++ b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts @@ -68,7 +68,8 @@ suite('TokenizationSupport2Adapter', () => { tokenColorMap: [] }; } - + setColorMapOverride(colorMapOverride: Color[] | null): void { + } public getFileIconTheme(): IFileIconTheme { return { hasFileIcons: false, @@ -107,15 +108,15 @@ suite('TokenizationSupport2Adapter', () => { const adapter = new TokenizationSupport2Adapter(new MockThemeService(), languageIdentifier, new BadTokensProvider()); - const actualClassicTokens = adapter.tokenize('whatever', MockState.INSTANCE, offsetDelta); - assert.deepEqual(actualClassicTokens.tokens, expectedClassicTokens); + const actualClassicTokens = adapter.tokenize('whatever', true, MockState.INSTANCE, offsetDelta); + assert.deepStrictEqual(actualClassicTokens.tokens, expectedClassicTokens); - const actualModernTokens = adapter.tokenize2('whatever', MockState.INSTANCE, offsetDelta); + const actualModernTokens = adapter.tokenize2('whatever', true, MockState.INSTANCE, offsetDelta); const modernTokens: number[] = []; for (let i = 0; i < actualModernTokens.tokens.length; i++) { modernTokens[i] = actualModernTokens.tokens[i]; } - assert.deepEqual(modernTokens, expectedModernTokens); + assert.deepStrictEqual(modernTokens, expectedModernTokens); } test('tokens always start at index 0 (no offset delta)', () => { diff --git a/src/vs/editor/standalone/test/monarch/monarch.test.ts b/src/vs/editor/standalone/test/monarch/monarch.test.ts index 89eb03ce..61fcafe5 100644 --- a/src/vs/editor/standalone/test/monarch/monarch.test.ts +++ b/src/vs/editor/standalone/test/monarch/monarch.test.ts @@ -68,39 +68,154 @@ suite('Monarch', () => { const actualTokens: Token[][] = []; let state = tokenizer.getInitialState(); for (const line of lines) { - const result = tokenizer.tokenize(line, state, 0); + const result = tokenizer.tokenize(line, true, state, 0); actualTokens.push(result.tokens); state = result.endState; } - assert.deepEqual(actualTokens, [ + assert.deepStrictEqual(actualTokens, [ [ - { 'offset': 0, 'type': 'source.test1', 'language': 'test1' }, - { 'offset': 12, 'type': 'string.quote.test1', 'language': 'test1' }, - { 'offset': 15, 'type': 'token.sql', 'language': 'sql' }, - { 'offset': 61, 'type': 'string.quote.test1', 'language': 'test1' }, - { 'offset': 64, 'type': 'source.test1', 'language': 'test1' } + new Token(0, 'source.test1', 'test1'), + new Token(12, 'string.quote.test1', 'test1'), + new Token(15, 'token.sql', 'sql'), + new Token(61, 'string.quote.test1', 'test1'), + new Token(64, 'source.test1', 'test1') ], [ - { 'offset': 0, 'type': 'source.test1', 'language': 'test1' }, - { 'offset': 12, 'type': 'string.quote.test1', 'language': 'test1' } + new Token(0, 'source.test1', 'test1'), + new Token(12, 'string.quote.test1', 'test1') ], [ - { 'offset': 0, 'type': 'token.sql', 'language': 'sql' } + new Token(0, 'token.sql', 'sql') ], [ - { 'offset': 0, 'type': 'token.sql', 'language': 'sql' } + new Token(0, 'token.sql', 'sql') ], [ - { 'offset': 0, 'type': 'token.sql', 'language': 'sql' } + new Token(0, 'token.sql', 'sql') ], [ - { 'offset': 0, 'type': 'string.quote.test1', 'language': 'test1' }, - { 'offset': 3, 'type': 'source.test1', 'language': 'test1' } + new Token(0, 'string.quote.test1', 'test1'), + new Token(3, 'source.test1', 'test1') ] ]); innerModeTokenizationRegistration.dispose(); innerModeRegistration.dispose(); }); + test('microsoft/monaco-editor#1235: Empty Line Handling', () => { + const modeService = new ModeServiceImpl(); + const tokenizer = createMonarchTokenizer(modeService, 'test', { + tokenizer: { + root: [ + { include: '@comments' }, + ], + + comments: [ + [/\/\/$/, 'comment'], // empty single-line comment + [/\/\//, 'comment', '@comment_cpp'], + ], + + comment_cpp: [ + [/(?:[^\\]|(?:\\.))+$/, 'comment', '@pop'], + [/.+$/, 'comment'], + [/$/, 'comment', '@pop'] + // No possible rule to detect an empty line and @pop? + ], + }, + }); + + const lines = [ + `// This comment \\`, + ` continues on the following line`, + ``, + `// This comment does NOT continue \\\\`, + ` because the escape char was itself escaped`, + ``, + `// This comment DOES continue because \\\\\\`, + ` the 1st '\\' escapes the 2nd; the 3rd escapes EOL`, + ``, + `// This comment continues to the following line \\`, + ``, + `But the line was empty. This line should not be commented.`, + ]; + + const actualTokens: Token[][] = []; + let state = tokenizer.getInitialState(); + for (const line of lines) { + const result = tokenizer.tokenize(line, true, state, 0); + actualTokens.push(result.tokens); + state = result.endState; + } + + assert.deepStrictEqual(actualTokens, [ + [new Token(0, 'comment.test', 'test')], + [new Token(0, 'comment.test', 'test')], + [], + [new Token(0, 'comment.test', 'test')], + [new Token(0, 'source.test', 'test')], + [], + [new Token(0, 'comment.test', 'test')], + [new Token(0, 'comment.test', 'test')], + [], + [new Token(0, 'comment.test', 'test')], + [], + [new Token(0, 'source.test', 'test')] + ]); + + }); + + test('microsoft/monaco-editor#2265: Exit a state at end of line', () => { + const modeService = new ModeServiceImpl(); + const tokenizer = createMonarchTokenizer(modeService, 'test', { + includeLF: true, + tokenizer: { + root: [ + [/^\*/, '', '@inner'], + [/\:\*/, '', '@inner'], + [/[^*:]+/, 'string'], + [/[*:]/, 'string'] + ], + inner: [ + [/\n/, '', '@pop'], + [/\d+/, 'number'], + [/[^\d]+/, ''] + ] + } + }); + + const lines = [ + `PRINT 10 * 20`, + `*FX200, 3`, + `PRINT 2*3:*FX200, 3` + ]; + + const actualTokens: Token[][] = []; + let state = tokenizer.getInitialState(); + for (const line of lines) { + const result = tokenizer.tokenize(line, true, state, 0); + actualTokens.push(result.tokens); + state = result.endState; + } + + assert.deepStrictEqual(actualTokens, [ + [ + new Token(0, 'string.test', 'test'), + ], + [ + new Token(0, '', 'test'), + new Token(3, 'number.test', 'test'), + new Token(6, '', 'test'), + new Token(8, 'number.test', 'test'), + ], + [ + new Token(0, 'string.test', 'test'), + new Token(9, '', 'test'), + new Token(13, 'number.test', 'test'), + new Token(16, '', 'test'), + new Token(18, 'number.test', 'test'), + ] + ]); + }); + }); diff --git a/src/vs/editor/test/browser/commands/shiftCommand.test.ts b/src/vs/editor/test/browser/commands/shiftCommand.test.ts index 755aac5b..c31f8e4b 100644 --- a/src/vs/editor/test/browser/commands/shiftCommand.test.ts +++ b/src/vs/editor/test/browser/commands/shiftCommand.test.ts @@ -964,7 +964,7 @@ suite('Editor Commands - ShiftCommand', () => { autoIndent: EditorAutoIndentStrategy.Full, }); let actual = getEditOperation(model, op); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); } @@ -979,7 +979,7 @@ suite('Editor Commands - ShiftCommand', () => { autoIndent: EditorAutoIndentStrategy.Full, }); let actual = getEditOperation(model, op); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); } }); diff --git a/src/vs/editor/test/browser/commands/sideEditing.test.ts b/src/vs/editor/test/browser/commands/sideEditing.test.ts index be07a568..f8a8bbae 100644 --- a/src/vs/editor/test/browser/commands/sideEditing.test.ts +++ b/src/vs/editor/test/browser/commands/sideEditing.test.ts @@ -19,10 +19,10 @@ function testCommand(lines: string[], selections: Selection[], edits: IIdentifie model.applyEdits(edits); - assert.deepEqual(model.getLinesContent(), expectedLines); + assert.deepStrictEqual(model.getLinesContent(), expectedLines); let actualSelections = viewModel.getSelections(); - assert.deepEqual(actualSelections.map(s => s.toString()), expectedSelections.map(s => s.toString())); + assert.deepStrictEqual(actualSelections.map(s => s.toString()), expectedSelections.map(s => s.toString())); }); } @@ -202,7 +202,7 @@ suite('SideEditing', () => { forceMoveMarkers: editForceMoveMarkers }]); const actual = viewModel.getSelection(); - assert.deepEqual(actual.toString(), expected.toString(), msg); + assert.deepStrictEqual(actual.toString(), expected.toString(), msg); }); } diff --git a/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts b/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts index af0c64e5..d9fdfc1b 100644 --- a/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts +++ b/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts @@ -37,14 +37,14 @@ function assertTrimTrailingWhitespaceCommand(text: string[], expected: IIdentifi return withEditorModel(text, (model) => { let op = new TrimTrailingWhitespaceCommand(new Selection(1, 1, 1, 1), []); let actual = getEditOperation(model, op); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); } function assertTrimTrailingWhitespace(text: string[], cursors: Position[], expected: IIdentifiedSingleEditOperation[]): void { return withEditorModel(text, (model) => { let actual = trimTrailingWhitespace(model, cursors); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); } diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index cb202a3f..489af471 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -115,7 +115,7 @@ function assertCursor(viewModel: ViewModel, what: Position | Selection | Selecti let actual = viewModel.getSelections().map(s => s.toString()); let expected = selections.map(s => s.toString()); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } suite('Editor Controller - Cursor', () => { @@ -795,11 +795,11 @@ suite('Editor Controller - Cursor', () => { viewModel.onEvent((e) => { if (e.kind === OutgoingViewModelEventKind.CursorStateChanged) { events++; - assert.deepEqual(e.selections, [new Selection(1, 2, 1, 2)]); + assert.deepStrictEqual(e.selections, [new Selection(1, 2, 1, 2)]); } }); moveTo(editor, viewModel, 1, 2); - assert.equal(events, 1, 'receives 1 event'); + assert.strictEqual(events, 1, 'receives 1 event'); }); }); @@ -809,11 +809,11 @@ suite('Editor Controller - Cursor', () => { viewModel.onEvent((e) => { if (e.kind === OutgoingViewModelEventKind.CursorStateChanged) { events++; - assert.deepEqual(e.selections, [new Selection(1, 1, 1, 2)]); + assert.deepStrictEqual(e.selections, [new Selection(1, 1, 1, 2)]); } }); moveTo(editor, viewModel, 1, 2, true); - assert.equal(events, 1, 'receives 1 event'); + assert.strictEqual(events, 1, 'receives 1 event'); }); }); @@ -1311,7 +1311,7 @@ suite('Editor Controller - Regression tests', () => { // Check that indenting maintains the selection start at column 1 CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.deepEqual(viewModel.getSelection(), new Selection(1, 1, 1, 14)); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(1, 1, 1, 14)); }); model.dispose(); @@ -1330,49 +1330,49 @@ suite('Editor Controller - Regression tests', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n', 'assert1'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n', 'assert1'); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t', 'assert2'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t', 'assert2'); viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\n\t', 'assert3'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t\n\t', 'assert3'); viewModel.type('x'); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\n\tx', 'assert4'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t\n\tx', 'assert4'); CoreNavigationCommands.CursorLeft.runCoreEditorCommand(viewModel, {}); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\n\tx', 'assert5'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t\n\tx', 'assert5'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\nx', 'assert6'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t\nx', 'assert6'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\tx', 'assert7'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\tx', 'assert7'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert8'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\nx', 'assert8'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'x', 'assert9'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'x', 'assert9'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert10'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\nx', 'assert10'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\nx', 'assert11'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t\nx', 'assert11'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\n\tx', 'assert12'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t\n\tx', 'assert12'); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\nx', 'assert13'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t\nx', 'assert13'); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert14'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\nx', 'assert14'); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'x', 'assert15'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'x', 'assert15'); }); model.dispose(); @@ -1387,13 +1387,13 @@ suite('Editor Controller - Regression tests', () => { assertCursor(viewModel, new Position(1, 1)); model.setEOL(EndOfLineSequence.LF); - assert.equal(model.getValue(), 'Hello\nworld'); + assert.strictEqual(model.getValue(), 'Hello\nworld'); model.pushEOL(EndOfLineSequence.CRLF); - assert.equal(model.getValue(), 'Hello\r\nworld'); + assert.strictEqual(model.getValue(), 'Hello\r\nworld'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), 'Hello\nworld'); + assert.strictEqual(model.getValue(), 'Hello\nworld'); }); }); @@ -1415,10 +1415,10 @@ suite('Editor Controller - Regression tests', () => { editor.setSelection(new Selection(1, 1, 1, 2)); viewModel.type('%', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), '%\'%👁\'', 'assert1'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '%\'%👁\'', 'assert1'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\'👁\'', 'assert2'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\'👁\'', 'assert2'); }); model.dispose(); @@ -1433,50 +1433,50 @@ suite('Editor Controller - Regression tests', () => { viewModel.type(' ', 'keyboard'); viewModel.type('world', 'keyboard'); viewModel.type(' ', 'keyboard'); - assert.equal(model.getLineContent(1), 'Hello world '); + assert.strictEqual(model.getLineContent(1), 'Hello world '); assertCursor(viewModel, new Position(1, 13)); moveLeft(editor, viewModel); moveRight(editor, viewModel); model.pushEditOperations([], [EditOperation.replaceMove(new Range(1, 12, 1, 13), '')], () => []); - assert.equal(model.getLineContent(1), 'Hello world'); + assert.strictEqual(model.getLineContent(1), 'Hello world'); assertCursor(viewModel, new Position(1, 12)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello world '); + assert.strictEqual(model.getLineContent(1), 'Hello world '); assertCursor(viewModel, new Selection(1, 12, 1, 13)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello world'); + assert.strictEqual(model.getLineContent(1), 'Hello world'); assertCursor(viewModel, new Position(1, 12)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello'); + assert.strictEqual(model.getLineContent(1), 'Hello'); assertCursor(viewModel, new Position(1, 6)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ''); + assert.strictEqual(model.getLineContent(1), ''); assertCursor(viewModel, new Position(1, 1)); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello'); + assert.strictEqual(model.getLineContent(1), 'Hello'); assertCursor(viewModel, new Position(1, 6)); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello world'); + assert.strictEqual(model.getLineContent(1), 'Hello world'); assertCursor(viewModel, new Position(1, 12)); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello world '); + assert.strictEqual(model.getLineContent(1), 'Hello world '); assertCursor(viewModel, new Position(1, 13)); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello world'); + assert.strictEqual(model.getLineContent(1), 'Hello world'); assertCursor(viewModel, new Position(1, 12)); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello world'); + assert.strictEqual(model.getLineContent(1), 'Hello world'); assertCursor(viewModel, new Position(1, 12)); }); @@ -1498,7 +1498,7 @@ suite('Editor Controller - Regression tests', () => { assertCursor(viewModel, new Selection(1, 6, 1, 6)); CoreEditingCommands.Outdent.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' function baz() {'); + assert.strictEqual(model.getLineContent(1), ' function baz() {'); assertCursor(viewModel, new Selection(1, 5, 1, 5)); }); @@ -1518,7 +1518,7 @@ suite('Editor Controller - Regression tests', () => { assertCursor(viewModel, new Selection(1, 7, 1, 7)); CoreEditingCommands.Outdent.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' '); + assert.strictEqual(model.getLineContent(1), ' '); assertCursor(viewModel, new Selection(1, 5, 1, 5)); }); @@ -1540,7 +1540,7 @@ suite('Editor Controller - Regression tests', () => { assertCursor(viewModel, new Selection(1, 9, 1, 9)); CoreEditingCommands.Outdent.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' '); + assert.strictEqual(model.getLineContent(1), ' '); assertCursor(viewModel, new Selection(1, 5, 1, 5)); }); @@ -1568,7 +1568,7 @@ suite('Editor Controller - Regression tests', () => { assertCursor(viewModel, new Selection(7, 1, 7, 1)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(7), '\t'); + assert.strictEqual(model.getLineContent(7), '\t'); assertCursor(viewModel, new Selection(7, 2, 7, 2)); }); @@ -1588,8 +1588,8 @@ suite('Editor Controller - Regression tests', () => { assertCursor(viewModel, new Selection(2, 1, 2, 1)); viewModel.cut('keyboard'); - assert.equal(model.getLineCount(), 1); - assert.equal(model.getLineContent(1), 'asdasd'); + assert.strictEqual(model.getLineCount(), 1); + assert.strictEqual(model.getLineContent(1), 'asdasd'); }); @@ -1604,12 +1604,12 @@ suite('Editor Controller - Regression tests', () => { assertCursor(viewModel, new Selection(2, 1, 2, 1)); viewModel.cut('keyboard'); - assert.equal(model.getLineCount(), 1); - assert.equal(model.getLineContent(1), 'asdasd'); + assert.strictEqual(model.getLineCount(), 1); + assert.strictEqual(model.getLineContent(1), 'asdasd'); viewModel.cut('keyboard'); - assert.equal(model.getLineCount(), 1); - assert.equal(model.getLineContent(1), ''); + assert.strictEqual(model.getLineCount(), 1); + assert.strictEqual(model.getLineContent(1), ''); }); }); @@ -1651,8 +1651,8 @@ suite('Editor Controller - Regression tests', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assertCursor(viewModel, new Selection(1, 14, 1, 14)); - assert.equal(model.getLineCount(), 1); - assert.equal(model.getLineContent(1), 'function baz(;'); + assert.strictEqual(model.getLineCount(), 1); + assert.strictEqual(model.getLineContent(1), 'function baz(;'); }); model.dispose(); @@ -1671,9 +1671,9 @@ suite('Editor Controller - Regression tests', () => { viewModel.paste('line1\n', true); - assert.equal(model.getLineContent(1), 'line1'); - assert.equal(model.getLineContent(2), 'line1'); - assert.equal(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(1), 'line1'); + assert.strictEqual(model.getLineContent(2), 'line1'); + assert.strictEqual(model.getLineContent(3), ''); }); }); @@ -1689,10 +1689,10 @@ suite('Editor Controller - Regression tests', () => { viewModel.paste('line1\n', true); - assert.equal(model.getLineContent(1), 'line1'); - assert.equal(model.getLineContent(2), 'line line1'); - assert.equal(model.getLineContent(3), ' 2'); - assert.equal(model.getLineContent(4), 'line3'); + assert.strictEqual(model.getLineContent(1), 'line1'); + assert.strictEqual(model.getLineContent(2), 'line line1'); + assert.strictEqual(model.getLineContent(3), ' 2'); + assert.strictEqual(model.getLineContent(4), 'line3'); }); }); @@ -1715,7 +1715,7 @@ suite('Editor Controller - Regression tests', () => { ] ); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'a', 'bline1', 'c', @@ -1747,7 +1747,7 @@ suite('Editor Controller - Regression tests', () => { null ); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'aaa', 'bbb', 'ccc', @@ -1790,7 +1790,7 @@ suite('Editor Controller - Regression tests', () => { null ); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'aaa', 'bbb', 'ccc', @@ -1815,7 +1815,7 @@ suite('Editor Controller - Regression tests', () => { null ); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'aline1', 'bline2', 'cline3' @@ -1839,7 +1839,7 @@ suite('Editor Controller - Regression tests', () => { null ); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'aline1', 'bline2', 'cline3' @@ -1869,26 +1869,26 @@ suite('Editor Controller - Regression tests', () => { }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ '\t just some text' ].join('\n'), '001'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ ' some lines', ' and more lines', ' just some text', ].join('\n'), '002'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'some lines', 'and more lines', 'just some text', ].join('\n'), '003'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'some lines', 'and more lines', 'just some text', @@ -1911,7 +1911,7 @@ suite('Editor Controller - Regression tests', () => { viewModel.type('😍', 'keyboard'); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'some lines', 'and more lines', '😍just some text', @@ -1933,7 +1933,7 @@ suite('Editor Controller - Regression tests', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { moveTo(editor, viewModel, 3, 2, false); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(3), '\t \tx: 3'); + assert.strictEqual(model.getLineContent(3), '\t \tx: 3'); }); model.dispose(); @@ -1954,7 +1954,7 @@ suite('Editor Controller - Regression tests', () => { moveTo(editor, viewModel, 1, 15, false); moveTo(editor, viewModel, 1, 22, true); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'var foo = 123;\t// this is a comment'); + assert.strictEqual(model.getLineContent(1), 'var foo = 123;\t// this is a comment'); }); model.dispose(); @@ -1982,8 +1982,8 @@ suite('Editor Controller - Regression tests', () => { CoreNavigationCommands.WordSelectDrag.runCoreEditorCommand(viewModel, args); } - assert.equal(viewModel.getSelection().startColumn, 1, 'TEST FOR ' + col); - assert.equal(viewModel.getSelection().endColumn, expectedCol, 'TEST FOR ' + col); + assert.strictEqual(viewModel.getSelection().startColumn, 1, 'TEST FOR ' + col); + assert.strictEqual(viewModel.getSelection().endColumn, expectedCol, 'TEST FOR ' + col); } assertWordRight(1, ' '.length + 1); @@ -2048,10 +2048,10 @@ suite('Editor Controller - Regression tests', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { CoreNavigationCommands.WordSelect.runCoreEditorCommand(viewModel, { position: new Position(1, 8) }); - assert.deepEqual(viewModel.getSelection(), new Selection(1, 6, 1, 10)); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(1, 6, 1, 10)); CoreNavigationCommands.WordSelectDrag.runCoreEditorCommand(viewModel, { position: new Position(1, 8) }); - assert.deepEqual(viewModel.getSelection(), new Selection(1, 6, 1, 10)); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(1, 6, 1, 10)); }); model.dispose(); @@ -2066,7 +2066,7 @@ suite('Editor Controller - Regression tests', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { CoreNavigationCommands.WordSelect.runCoreEditorCommand(viewModel, { position: new Position(1, 5) }); - assert.deepEqual(viewModel.getSelection(), new Selection(1, 5, 1, 8)); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(1, 5, 1, 8)); }); model.dispose(); @@ -2090,11 +2090,11 @@ suite('Editor Controller - Regression tests', () => { viewModel.replacePreviousChar('せんせい', 4); viewModel.replacePreviousChar('せんせい', 4); - assert.equal(model.getLineContent(1), 'せんせい'); + assert.strictEqual(model.getLineContent(1), 'せんせい'); assertCursor(viewModel, new Position(1, 5)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ''); + assert.strictEqual(model.getLineContent(1), ''); assertCursor(viewModel, new Position(1, 1)); }); }); @@ -2121,11 +2121,11 @@ suite('Editor Controller - Regression tests', () => { viewModel.type('n', 'keyboard'); for (let i = 0; i < LINE_CNT; i++) { - assert.equal(model.getLineContent(i + 1), 'nnasd', 'line #' + (i + 1)); + assert.strictEqual(model.getLineContent(i + 1), 'nnasd', 'line #' + (i + 1)); } - assert.equal(viewModel.getSelections().length, LINE_CNT); - assert.equal(viewModel.getSelections()[LINE_CNT - 1].startLineNumber, LINE_CNT); + assert.strictEqual(viewModel.getSelections().length, LINE_CNT); + assert.strictEqual(viewModel.getSelections()[LINE_CNT - 1].startLineNumber, LINE_CNT); }); }); @@ -2349,6 +2349,37 @@ suite('Editor Controller - Regression tests', () => { }); }); + test('issue #112301: new stickyTabStops feature interferes with word wrap', () => { + withTestCodeEditor([ + [ + 'function hello() {', + ' console.log(`this is a long console message`)', + '}', + ].join('\n') + ], { wordWrap: 'wordWrapColumn', wordWrapColumn: 32, stickyTabStops: true }, (editor, viewModel) => { + viewModel.setSelections('test', [ + new Selection(2, 31, 2, 31) + ]); + moveRight(editor, viewModel, false); + assertCursor(viewModel, new Position(2, 32)); + + moveRight(editor, viewModel, false); + assertCursor(viewModel, new Position(2, 33)); + + moveRight(editor, viewModel, false); + assertCursor(viewModel, new Position(2, 34)); + + moveLeft(editor, viewModel, false); + assertCursor(viewModel, new Position(2, 33)); + + moveLeft(editor, viewModel, false); + assertCursor(viewModel, new Position(2, 32)); + + moveLeft(editor, viewModel, false); + assertCursor(viewModel, new Position(2, 31)); + }); + }); + test('issue #44805: Should not be able to undo in readonly editor', () => { let model = createTextModel( [ @@ -2361,10 +2392,10 @@ suite('Editor Controller - Regression tests', () => { range: new Range(1, 1, 1, 1), text: 'Hello world!' }], () => [new Selection(1, 1, 1, 1)]); - assert.equal(model.getValue(EndOfLinePreference.LF), 'Hello world!'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'Hello world!'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'Hello world!'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'Hello world!'); }); model.dispose(); @@ -2375,7 +2406,7 @@ suite('Editor Controller - Regression tests', () => { const tokenizationSupport: ITokenizationSupport = { getInitialState: () => NULL_STATE, tokenize: undefined!, - tokenize2: (line: string, state: IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: IState): TokenizationResult2 => { return new TokenizationResult2(new Uint32Array(0), state); } }; @@ -2426,8 +2457,8 @@ suite('Editor Controller - Regression tests', () => { viewModel.type('\'', 'keyboard'); - assert.equal(model.getLineContent(1), 'const a = \'foo\';'); - assert.equal(model.getLineContent(2), 'const b = \'\''); + assert.strictEqual(model.getLineContent(1), 'const a = \'foo\';'); + assert.strictEqual(model.getLineContent(2), 'const b = \'\''); }); model.dispose(); @@ -2518,7 +2549,7 @@ suite('Editor Controller - Regression tests', () => { new Selection(2, 1, 2, 1) ]); viewModel.paste('something\n', true); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'abc123', 'something', '' @@ -2542,22 +2573,22 @@ suite('Editor Controller - Regression tests', () => { ]); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'สวัสด'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'สวัสด'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'สวัส'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'สวัส'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'สวั'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'สวั'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'สว'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'สว'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'ส'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'ส'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), ''); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), ''); }); model.dispose(); @@ -2578,8 +2609,8 @@ suite('Editor Controller - Cursor Configuration', () => { }, (editor, model, viewModel) => { CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(1, 21), source: 'keyboard' }); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' \tMy First Line\t '); - assert.equal(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(1), ' \tMy First Line\t '); + assert.strictEqual(model.getLineContent(2), ' '); }); }); @@ -2602,56 +2633,56 @@ suite('Editor Controller - Cursor Configuration', () => { // Tab on column 1 CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 1) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' My Second Line123'); + assert.strictEqual(model.getLineContent(2), ' My Second Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 2 - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 2) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'M y Second Line123'); + assert.strictEqual(model.getLineContent(2), 'M y Second Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 3 - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 3) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 4 - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 4) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 5 - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 5) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'My S econd Line123'); + assert.strictEqual(model.getLineContent(2), 'My S econd Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 5 - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 5) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'My S econd Line123'); + assert.strictEqual(model.getLineContent(2), 'My S econd Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 13 - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 13) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'My Second Li ne123'); + assert.strictEqual(model.getLineContent(2), 'My Second Li ne123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 14 - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 14) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'My Second Lin e123'); + assert.strictEqual(model.getLineContent(2), 'My Second Lin e123'); }); model.dispose(); @@ -2669,7 +2700,7 @@ suite('Editor Controller - Cursor Configuration', () => { assertCursor(viewModel, new Selection(1, 7, 1, 7)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.CRLF), '\thello\r\n '); + assert.strictEqual(model.getValue(EndOfLinePreference.CRLF), '\thello\r\n '); }); mode.dispose(); }); @@ -2686,7 +2717,7 @@ suite('Editor Controller - Cursor Configuration', () => { assertCursor(viewModel, new Selection(1, 7, 1, 7)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.CRLF), '\thello\r\n '); + assert.strictEqual(model.getValue(EndOfLinePreference.CRLF), '\thello\r\n '); }); mode.dispose(); }); @@ -2703,7 +2734,7 @@ suite('Editor Controller - Cursor Configuration', () => { assertCursor(viewModel, new Selection(1, 7, 1, 7)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.CRLF), '\thell(\r\n \r\n )'); + assert.strictEqual(model.getValue(EndOfLinePreference.CRLF), '\thell(\r\n \r\n )'); }); mode.dispose(); }); @@ -2721,14 +2752,14 @@ suite('Editor Controller - Cursor Configuration', () => { // Move cursor to the end, verify that we do not trim whitespaces if line has values moveTo(editor, viewModel, 1, model.getLineContent(1).length + 1); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' some line abc '); - assert.equal(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(1), ' some line abc '); + assert.strictEqual(model.getLineContent(2), ' '); // Try to enter again, we should trimmed previous line viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' some line abc '); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), ' '); + assert.strictEqual(model.getLineContent(1), ' some line abc '); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), ' '); }); }); @@ -2740,16 +2771,47 @@ suite('Editor Controller - Cursor Configuration', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, model.getLineContent(1).length + 1); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' '); - assert.equal(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(1), ' '); + assert.strictEqual(model.getLineContent(2), ' '); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' '); - assert.equal(model.getLineContent(2), ''); - assert.equal(model.getLineContent(3), ' '); + assert.strictEqual(model.getLineContent(1), ' '); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), ' '); }); }); + test('issue #115033: indent and appendText', () => { + const mode = new class extends MockMode { + constructor() { + super(new LanguageIdentifier('onEnterMode', 3)); + this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + onEnterRules: [{ + beforeText: /.*/, + action: { + indentAction: IndentAction.Indent, + appendText: 'x' + } + }] + })); + } + }(); + usingCursor({ + text: [ + 'text' + ], + languageIdentifier: mode.getLanguageIdentifier(), + }, (editor, model, viewModel) => { + + moveTo(editor, viewModel, 1, 5); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getLineContent(1), 'text'); + assert.strictEqual(model.getLineContent(2), ' x'); + assertCursor(viewModel, new Position(2, 6)); + }); + mode.dispose(); + }); + test('issue #6862: Editor removes auto inserted indentation when formatting on type', () => { let mode = new OnEnterMode(IndentAction.IndentOutdent); usingCursor({ @@ -2761,9 +2823,9 @@ suite('Editor Controller - Cursor Configuration', () => { moveTo(editor, viewModel, 1, 32); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), 'function foo (params: string) {'); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), '}'); + assert.strictEqual(model.getLineContent(1), 'function foo (params: string) {'); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), '}'); class TestCommand implements ICommand { @@ -2781,9 +2843,9 @@ suite('Editor Controller - Cursor Configuration', () => { } viewModel.executeCommand(new TestCommand(), 'autoFormat'); - assert.equal(model.getLineContent(1), 'function foo(params: string) {'); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), '}'); + assert.strictEqual(model.getLineContent(1), 'function foo(params: string) {'); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), '}'); }); mode.dispose(); }); @@ -2803,27 +2865,27 @@ suite('Editor Controller - Cursor Configuration', () => { moveTo(editor, viewModel, 3, 1); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' if (a) {'); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), ' '); - assert.equal(model.getLineContent(4), ''); - assert.equal(model.getLineContent(5), ' }'); + assert.strictEqual(model.getLineContent(1), ' if (a) {'); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), ' '); + assert.strictEqual(model.getLineContent(4), ''); + assert.strictEqual(model.getLineContent(5), ' }'); moveTo(editor, viewModel, 4, 1); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' if (a) {'); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), ''); - assert.equal(model.getLineContent(4), ' '); - assert.equal(model.getLineContent(5), ' }'); + assert.strictEqual(model.getLineContent(1), ' if (a) {'); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(4), ' '); + assert.strictEqual(model.getLineContent(5), ' }'); moveTo(editor, viewModel, 5, model.getLineMaxColumn(5)); viewModel.type('something', 'keyboard'); - assert.equal(model.getLineContent(1), ' if (a) {'); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), ''); - assert.equal(model.getLineContent(4), ''); - assert.equal(model.getLineContent(5), ' }something'); + assert.strictEqual(model.getLineContent(1), ' if (a) {'); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(4), ''); + assert.strictEqual(model.getLineContent(5), ' }something'); }); model.dispose(); @@ -2841,46 +2903,46 @@ suite('Editor Controller - Cursor Configuration', () => { // Move cursor to the end, verify that we do not trim whitespaces if line has values moveTo(editor, viewModel, 1, model.getLineContent(1).length + 1); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' some line abc '); - assert.equal(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(1), ' some line abc '); + assert.strictEqual(model.getLineContent(2), ' '); // Try to enter again, we should trimmed previous line viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' some line abc '); - assert.equal(model.getLineContent(2), ''); - assert.equal(model.getLineContent(3), ' '); + assert.strictEqual(model.getLineContent(1), ' some line abc '); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), ' '); // More whitespaces CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' some line abc '); - assert.equal(model.getLineContent(2), ''); - assert.equal(model.getLineContent(3), ' '); + assert.strictEqual(model.getLineContent(1), ' some line abc '); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), ' '); // Enter and verify that trimmed again viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' some line abc '); - assert.equal(model.getLineContent(2), ''); - assert.equal(model.getLineContent(3), ''); - assert.equal(model.getLineContent(4), ' '); + assert.strictEqual(model.getLineContent(1), ' some line abc '); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(4), ' '); // Trimmed if we will keep only text moveTo(editor, viewModel, 1, 5); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' '); - assert.equal(model.getLineContent(2), ' some line abc '); - assert.equal(model.getLineContent(3), ''); - assert.equal(model.getLineContent(4), ''); - assert.equal(model.getLineContent(5), ''); + assert.strictEqual(model.getLineContent(1), ' '); + assert.strictEqual(model.getLineContent(2), ' some line abc '); + assert.strictEqual(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(4), ''); + assert.strictEqual(model.getLineContent(5), ''); // Trimmed if we will keep only text by selection moveTo(editor, viewModel, 2, 5); moveTo(editor, viewModel, 3, 1, true); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' '); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), ' '); - assert.equal(model.getLineContent(4), ''); - assert.equal(model.getLineContent(5), ''); + assert.strictEqual(model.getLineContent(1), ' '); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), ' '); + assert.strictEqual(model.getLineContent(4), ''); + assert.strictEqual(model.getLineContent(5), ''); }); model.dispose(); @@ -2901,7 +2963,7 @@ suite('Editor Controller - Cursor Configuration', () => { moveTo(editor, viewModel, 3, model.getLineMaxColumn(3)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ ' function f() {', ' // I\'m gonna copy this line', ' return 3;', @@ -2911,7 +2973,7 @@ suite('Editor Controller - Cursor Configuration', () => { assertCursor(viewModel, new Position(4, model.getLineMaxColumn(4))); viewModel.paste(' // I\'m gonna copy this line\n', true); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ ' function f() {', ' // I\'m gonna copy this line', ' return 3;', @@ -2941,7 +3003,7 @@ suite('Editor Controller - Cursor Configuration', () => { editor.setSelections([new Selection(4, 10, 4, 10)]); viewModel.paste(' // I\'m gonna copy this line\n', true); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ ' function f() {', ' // I\'m gonna copy this line', ' // Another line', @@ -2968,7 +3030,7 @@ suite('Editor Controller - Cursor Configuration', () => { // DeleteLeft removes just one whitespace moveTo(editor, viewModel, 2, 9); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' a '); + assert.strictEqual(model.getLineContent(2), ' a '); }); model.dispose(); @@ -2987,54 +3049,54 @@ suite('Editor Controller - Cursor Configuration', () => { // DeleteLeft does not remove tab size, because some text exists before moveTo(editor, viewModel, 2, model.getLineContent(2).length + 1); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' a '); + assert.strictEqual(model.getLineContent(2), ' a '); // DeleteLeft removes tab size = 4 moveTo(editor, viewModel, 2, 9); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' a '); + assert.strictEqual(model.getLineContent(2), ' a '); // DeleteLeft removes tab size = 4 CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'a '); + assert.strictEqual(model.getLineContent(2), 'a '); // Undo DeleteLeft - get us back to original indentation CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' a '); + assert.strictEqual(model.getLineContent(2), ' a '); // Nothing is broken when cursor is in (1,1) moveTo(editor, viewModel, 1, 1); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' \t \t x'); + assert.strictEqual(model.getLineContent(1), ' \t \t x'); // DeleteLeft stops at tab stops even in mixed whitespace case moveTo(editor, viewModel, 1, 10); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' \t \t x'); + assert.strictEqual(model.getLineContent(1), ' \t \t x'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' \t \tx'); + assert.strictEqual(model.getLineContent(1), ' \t \tx'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' \tx'); + assert.strictEqual(model.getLineContent(1), ' \tx'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'x'); + assert.strictEqual(model.getLineContent(1), 'x'); // DeleteLeft on last line moveTo(editor, viewModel, 3, model.getLineContent(3).length + 1); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(3), ''); // DeleteLeft with removing new line symbol CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'x\n a '); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'x\n a '); // In case of selection DeleteLeft only deletes selected text moveTo(editor, viewModel, 2, 3); moveTo(editor, viewModel, 2, 4, true); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' a '); + assert.strictEqual(model.getLineContent(2), ' a '); }); model.dispose(); @@ -3052,55 +3114,55 @@ suite('Editor Controller - Cursor Configuration', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n', 'assert1'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n', 'assert1'); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t', 'assert2'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t', 'assert2'); viewModel.type('y', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty', 'assert2'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty', 'assert2'); viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\n\t', 'assert3'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty\n\t', 'assert3'); viewModel.type('x'); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert4'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert4'); CoreNavigationCommands.CursorLeft.runCoreEditorCommand(viewModel, {}); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert5'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert5'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert6'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert6'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\tyx', 'assert7'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\tyx', 'assert7'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\tx', 'assert8'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\tx', 'assert8'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert9'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\nx', 'assert9'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'x', 'assert10'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'x', 'assert10'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert11'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\nx', 'assert11'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert12'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert12'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert13'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert13'); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert14'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert14'); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert15'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\nx', 'assert15'); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'x', 'assert16'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'x', 'assert16'); }); model.dispose(); @@ -3124,8 +3186,8 @@ suite('Editor Controller - Cursor Configuration', () => { const afterVersion = model.getVersionId(); const afterAltVersion = model.getAlternativeVersionId(); - assert.notEqual(beforeVersion, afterVersion); - assert.equal(beforeAltVersion, afterAltVersion); + assert.notStrictEqual(beforeVersion, afterVersion); + assert.strictEqual(beforeAltVersion, afterAltVersion); }); model.dispose(); @@ -3181,7 +3243,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('}', 'keyboard'); assertCursor(viewModel, new Selection(2, 2, 2, 2)); - assert.equal(model.getLineContent(2), '}', '001'); + assert.strictEqual(model.getLineContent(2), '}', '001'); }); }); @@ -3274,7 +3336,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 1, 4, 1)); - assert.equal(model.getLineContent(3), 'return true;', '001'); + assert.strictEqual(model.getLineContent(3), 'return true;', '001'); }); }); @@ -3295,7 +3357,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(5, 1, 5, 1)); - assert.equal(model.getLineContent(4), '\t}', '001'); + assert.strictEqual(model.getLineContent(4), '\t}', '001'); }); }); @@ -3363,7 +3425,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(3, 16, 3, 16)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(3), ' if (true) {'); + assert.strictEqual(model.getLineContent(3), ' if (true) {'); assertCursor(viewModel, new Selection(4, 9, 4, 9)); }); }); @@ -3388,7 +3450,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(3, 16, 3, 16)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(3), ' if (true) {'); + assert.strictEqual(model.getLineContent(3), ' if (true) {'); assertCursor(viewModel, new Selection(4, 3, 4, 3)); }); }); @@ -3411,7 +3473,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(5, 4, 5, 4)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(5), '\t\t}'); + assert.strictEqual(model.getLineContent(5), '\t\t}'); assertCursor(viewModel, new Selection(6, 3, 6, 3)); }); }); @@ -3432,7 +3494,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 3, 4, 3)); - assert.equal(model.getLineContent(4), '\t\t true;', '001'); + assert.strictEqual(model.getLineContent(4), '\t\t true;', '001'); }); }); @@ -3452,7 +3514,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 3, 4, 3)); - assert.equal(model.getLineContent(4), '\t\treturn true;', '001'); + assert.strictEqual(model.getLineContent(4), '\t\treturn true;', '001'); }); }); @@ -3471,7 +3533,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 5, 4, 5)); - assert.equal(model.getLineContent(4), ' true;', '001'); + assert.strictEqual(model.getLineContent(4), ' true;', '001'); }); }); @@ -3491,14 +3553,14 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 2, 4, 2)); - assert.equal(model.getLineContent(4), '\t\treturn true;', '001'); + assert.strictEqual(model.getLineContent(4), '\t\treturn true;', '001'); moveTo(editor, viewModel, 4, 1, false); assertCursor(viewModel, new Selection(4, 1, 4, 1)); viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(5, 1, 5, 1)); - assert.equal(model.getLineContent(5), '\t\treturn true;', '002'); + assert.strictEqual(model.getLineContent(5), '\t\treturn true;', '002'); }); }); @@ -3518,14 +3580,14 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 3, 4, 3)); - assert.equal(model.getLineContent(4), '\t\t\treturn true;', '001'); + assert.strictEqual(model.getLineContent(4), '\t\t\treturn true;', '001'); moveTo(editor, viewModel, 4, 1, false); assertCursor(viewModel, new Selection(4, 1, 4, 1)); viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(5, 1, 5, 1)); - assert.equal(model.getLineContent(5), '\t\t\treturn true;', '002'); + assert.strictEqual(model.getLineContent(5), '\t\t\treturn true;', '002'); }); }); @@ -3544,12 +3606,12 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 2, 4, 2)); - assert.equal(model.getLineContent(4), ' return true;', '001'); + assert.strictEqual(model.getLineContent(4), ' return true;', '001'); moveTo(editor, viewModel, 4, 3, false); viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(5, 3, 5, 3)); - assert.equal(model.getLineContent(5), ' return true;', '002'); + assert.strictEqual(model.getLineContent(5), ' return true;', '002'); }); }); @@ -3577,12 +3639,12 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 4, 4, 4)); - assert.equal(model.getLineContent(4), ' return true;', '001'); + assert.strictEqual(model.getLineContent(4), ' return true;', '001'); moveTo(editor, viewModel, 9, 4, false); viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(10, 5, 10, 5)); - assert.equal(model.getLineContent(10), ' return true;', '001'); + assert.strictEqual(model.getLineContent(10), ' return true;', '001'); }); }); @@ -3604,7 +3666,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 3, 4, 3)); - assert.equal(model.getLineContent(4), ' return true;', '001'); + assert.strictEqual(model.getLineContent(4), ' return true;', '001'); }); }); @@ -3626,7 +3688,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(3, 8, 2, 12)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(3), '\treturn x;'); + assert.strictEqual(model.getLineContent(3), '\treturn x;'); assertCursor(viewModel, new Position(3, 2)); }); }); @@ -3649,7 +3711,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(2, 12, 3, 8)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(3), '\treturn x;'); + assert.strictEqual(model.getLineContent(3), '\treturn x;'); assertCursor(viewModel, new Position(3, 2)); }); }); @@ -3670,9 +3732,9 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(5, 3, 5, 3)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(6), '\t'); + assert.strictEqual(model.getLineContent(6), '\t'); assertCursor(viewModel, new Selection(6, 2, 6, 2)); - assert.equal(model.getLineContent(5), '\t}'); + assert.strictEqual(model.getLineContent(5), '\t}'); }); }); @@ -3690,7 +3752,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 2, 4, 2)); - assert.equal(model.getLineContent(4), '\t'); + assert.strictEqual(model.getLineContent(4), '\t'); }); }); @@ -3715,7 +3777,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(4, 1, 4, 1)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(4), '\t\t'); + assert.strictEqual(model.getLineContent(4), '\t\t'); }); model.dispose(); @@ -3743,7 +3805,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(4, 2, 4, 2)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(4), '\t\t\t'); + assert.strictEqual(model.getLineContent(4), '\t\t\t'); }); model.dispose(); @@ -3771,7 +3833,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(4, 1, 4, 1)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(4), '\t\t\t'); + assert.strictEqual(model.getLineContent(4), '\t\t\t'); }); model.dispose(); @@ -3798,7 +3860,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(4, 3, 4, 3)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(4), '\t\t\t\t'); + assert.strictEqual(model.getLineContent(4), '\t\t\t\t'); }); model.dispose(); @@ -3825,7 +3887,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(4, 4, 4, 4)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(4), '\t\t\t\t\t'); + assert.strictEqual(model.getLineContent(4), '\t\t\t\t\t'); }); model.dispose(); @@ -3849,11 +3911,11 @@ suite('Editor Controller - Indentation Rules', () => { moveTo(editor, viewModel, 3, 1); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' if (a) {'); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), ' '); - assert.equal(model.getLineContent(4), ''); - assert.equal(model.getLineContent(5), ' }'); + assert.strictEqual(model.getLineContent(1), ' if (a) {'); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), ' '); + assert.strictEqual(model.getLineContent(4), ''); + assert.strictEqual(model.getLineContent(5), ' }'); }); model.dispose(); @@ -3880,7 +3942,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(4, 7, 4, 7)); viewModel.type('d', 'keyboard'); - assert.equal(model.getLineContent(4), ' end'); + assert.strictEqual(model.getLineContent(4), ' end'); }); rubyMode.dispose(); @@ -3903,7 +3965,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('e', 'keyboard'); assertCursor(viewModel, new Selection(5, 4, 5, 4)); - assert.equal(model.getLineContent(5), '\t}e', 'This line should not decrease indent'); + assert.strictEqual(model.getLineContent(5), '\t}e', 'This line should not decrease indent'); }); }); @@ -3924,7 +3986,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type(' ', 'keyboard'); assertCursor(viewModel, new Selection(2, 4, 2, 4)); - assert.equal(model.getLineContent(2), '\t ) {', 'This line should not decrease indent'); + assert.strictEqual(model.getLineContent(2), '\t ) {', 'This line should not decrease indent'); }); }); @@ -3943,7 +4005,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('}', 'keyboard'); assertCursor(viewModel, new Selection(3, 2, 3, 2)); - assert.equal(model.getLineContent(3), '}'); + assert.strictEqual(model.getLineContent(3), '}'); }); }); @@ -3990,7 +4052,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(7, 6, 7, 6)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(), + assert.strictEqual(model.getValue(), [ 'class ItemCtrl {', ' getPropertiesByItemId(id) {', @@ -4010,6 +4072,47 @@ suite('Editor Controller - Indentation Rules', () => { mode.dispose(); }); + test('issue #115304: OnEnter broken for TS', () => { + class JSMode extends MockMode { + private static readonly _id = new LanguageIdentifier('indentRulesMode', 4); + constructor() { + super(JSMode._id); + this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + onEnterRules: javascriptOnEnterRules + })); + } + } + + const mode = new JSMode(); + const model = createTextModel( + [ + '/** */', + 'function f() {}', + ].join('\n'), + undefined, + mode.getLanguageIdentifier() + ); + + withTestCodeEditor(null, { model: model, autoIndent: 'advanced' }, (editor, viewModel) => { + moveTo(editor, viewModel, 1, 4, false); + assertCursor(viewModel, new Selection(1, 4, 1, 4)); + + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), + [ + '/**', + ' * ', + ' */', + 'function f() {}', + ].join('\n') + ); + assertCursor(viewModel, new Selection(2, 4, 2, 4)); + }); + + model.dispose(); + mode.dispose(); + }); + test('issue #38261: TAB key results in bizarre indentation in C++ mode ', () => { class CppMode extends MockMode { private static readonly _id = new LanguageIdentifier('indentRulesMode', 4); @@ -4054,7 +4157,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(8, 1, 8, 1)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), + assert.strictEqual(model.getValue(), [ 'int main() {', ' return 0;', @@ -4067,7 +4170,7 @@ suite('Editor Controller - Indentation Rules', () => { ')', ].join('\n') ); - assert.deepEqual(viewModel.getSelection(), new Selection(8, 3, 8, 3)); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(8, 3, 8, 3)); }); model.dispose(); @@ -4144,31 +4247,43 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(3, 19, 3, 19)); viewModel.type('\n', 'keyboard'); - assert.deepEqual(model.getLineContent(4), ' '); + assert.deepStrictEqual(model.getLineContent(4), ' '); moveTo(editor, viewModel, 5, 18, false); assertCursor(viewModel, new Selection(5, 18, 5, 18)); viewModel.type('\n', 'keyboard'); - assert.deepEqual(model.getLineContent(6), ' '); + assert.deepStrictEqual(model.getLineContent(6), ' '); moveTo(editor, viewModel, 7, 15, false); assertCursor(viewModel, new Selection(7, 15, 7, 15)); viewModel.type('\n', 'keyboard'); - assert.deepEqual(model.getLineContent(8), ' '); - assert.deepEqual(model.getLineContent(9), ' ]'); + assert.deepStrictEqual(model.getLineContent(8), ' '); + assert.deepStrictEqual(model.getLineContent(9), ' ]'); moveTo(editor, viewModel, 10, 18, false); assertCursor(viewModel, new Selection(10, 18, 10, 18)); viewModel.type('\n', 'keyboard'); - assert.deepEqual(model.getLineContent(11), ' ]'); + assert.deepStrictEqual(model.getLineContent(11), ' ]'); }); model.dispose(); mode.dispose(); }); + + test('issue #111128: Multicursor `Enter` issue with indentation', () => { + const model = createTextModel(' let a, b, c;', { detectIndentation: false, insertSpaces: false, tabSize: 4 }, mode.getLanguageIdentifier()); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + editor.setSelections([ + new Selection(1, 11, 1, 11), + new Selection(1, 14, 1, 14), + ]); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), ' let a,\n\t b,\n\t c;'); + }); + }); }); interface ICursorOpts { @@ -4218,7 +4333,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 1); viewModel.type('*', 'keyboard'); - assert.deepEqual(model.getLineContent(2), '*'); + assert.deepStrictEqual(model.getLineContent(2), '*'); }); mode.dispose(); }); @@ -4234,7 +4349,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 1); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(2), ' }'); + assert.deepStrictEqual(model.getLineContent(2), ' }'); }); mode.dispose(); }); @@ -4250,7 +4365,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 5); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(2), ' }'); + assert.deepStrictEqual(model.getLineContent(2), ' }'); }); mode.dispose(); }); @@ -4268,7 +4383,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 4, 1); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(4), ' } '); + assert.deepStrictEqual(model.getLineContent(4), ' } '); }); mode.dispose(); }); @@ -4286,7 +4401,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 4, 6); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(4), ' } }'); + assert.deepStrictEqual(model.getLineContent(4), ' } }'); }); mode.dispose(); }); @@ -4302,7 +4417,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 1); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(2), ' }// hello'); + assert.deepStrictEqual(model.getLineContent(2), ' }// hello'); }); mode.dispose(); }); @@ -4318,7 +4433,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 3); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(2), ' }'); + assert.deepStrictEqual(model.getLineContent(2), ' }'); }); mode.dispose(); }); @@ -4334,7 +4449,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 2); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(2), 'a}'); + assert.deepStrictEqual(model.getLineContent(2), 'a}'); }); mode.dispose(); }); @@ -4351,7 +4466,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 13); viewModel.type('*', 'keyboard'); - assert.deepEqual(model.getLineContent(2), ' ( 1 + 2 ) *'); + assert.deepStrictEqual(model.getLineContent(2), ' ( 1 + 2 ) *'); }); mode.dispose(); }); @@ -4370,8 +4485,8 @@ suite('ElectricCharacter', () => { changeText = e.changes[0].text; }); viewModel.type(')', 'keyboard'); - assert.deepEqual(model.getLineContent(1), '(div)'); - assert.deepEqual(changeText, ')'); + assert.deepStrictEqual(model.getLineContent(1), '(div)'); + assert.deepStrictEqual(changeText, ')'); }); mode.dispose(); }); @@ -4388,7 +4503,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 3); viewModel.type(')', 'keyboard'); - assert.deepEqual(model.getLineContent(3), '\t3)'); + assert.deepStrictEqual(model.getLineContent(3), '\t3)'); }); mode.dispose(); }); @@ -4404,7 +4519,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 3); viewModel.type('*', 'keyboard'); - assert.deepEqual(model.getLineContent(2), '/** */'); + assert.deepStrictEqual(model.getLineContent(2), '/** */'); }); mode.dispose(); }); @@ -4420,7 +4535,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 5); viewModel.type('*', 'keyboard'); - assert.deepEqual(model.getLineContent(2), ' /** */'); + assert.deepStrictEqual(model.getLineContent(2), ' /** */'); }); mode.dispose(); }); @@ -4437,7 +4552,7 @@ suite('ElectricCharacter', () => { moveTo(editor, viewModel, 2, 5); moveTo(editor, viewModel, 2, 1, true); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(2), '}'); + assert.deepStrictEqual(model.getLineContent(2), '}'); }); mode.dispose(); }); @@ -4513,7 +4628,7 @@ suite('autoClosingPairs', () => { let expected = lineContent.substr(0, column - 1) + expectedInsert + lineContent.substr(column - 1); moveTo(editor, viewModel, lineNumber, column); viewModel.type(chr, 'keyboard'); - assert.deepEqual(model.getLineContent(lineNumber), expected, message); + assert.deepStrictEqual(model.getLineContent(lineNumber), expected, message); model.undo(); } @@ -4783,12 +4898,12 @@ suite('autoClosingPairs', () => { // type a ` viewModel.type('`', 'keyboard'); - assert.equal(model.getValue(), '`var` a = `asd`'); + assert.strictEqual(model.getValue(), '`var` a = `asd`'); // type a ( viewModel.type('(', 'keyboard'); - assert.equal(model.getValue(), '`(var)` a = `(asd)`'); + assert.strictEqual(model.getValue(), '`(var)` a = `(asd)`'); }); usingCursor({ @@ -4808,7 +4923,7 @@ suite('autoClosingPairs', () => { // type a ` viewModel.type('`', 'keyboard'); - assert.equal(model.getValue(), '` a = asd'); + assert.strictEqual(model.getValue(), '` a = asd'); }); usingCursor({ @@ -4827,11 +4942,11 @@ suite('autoClosingPairs', () => { // type a ` viewModel.type('`', 'keyboard'); - assert.equal(model.getValue(), '`var` a = asd'); + assert.strictEqual(model.getValue(), '`var` a = asd'); // type a ( viewModel.type('(', 'keyboard'); - assert.equal(model.getValue(), '`(` a = asd'); + assert.strictEqual(model.getValue(), '`(` a = asd'); }); usingCursor({ @@ -4850,11 +4965,11 @@ suite('autoClosingPairs', () => { // type a ( viewModel.type('(', 'keyboard'); - assert.equal(model.getValue(), '(var) a = asd'); + assert.strictEqual(model.getValue(), '(var) a = asd'); // type a ` viewModel.type('`', 'keyboard'); - assert.equal(model.getValue(), '(`) a = asd'); + assert.strictEqual(model.getValue(), '(`) a = asd'); }); mode.dispose(); }); @@ -5047,50 +5162,50 @@ suite('autoClosingPairs', () => { // First gif model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste1 = teste\' ok'); - assert.equal(model.getLineContent(1), 'teste1 = teste\' ok'); + assert.strictEqual(model.getLineContent(1), 'teste1 = teste\' ok'); viewModel.setSelections('test', [new Selection(1, 1000, 1, 1000)]); typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste2 = teste \'ok'); - assert.equal(model.getLineContent(2), 'teste2 = teste \'ok\''); + assert.strictEqual(model.getLineContent(2), 'teste2 = teste \'ok\''); viewModel.setSelections('test', [new Selection(2, 1000, 2, 1000)]); typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste3 = teste" ok'); - assert.equal(model.getLineContent(3), 'teste3 = teste" ok'); + assert.strictEqual(model.getLineContent(3), 'teste3 = teste" ok'); viewModel.setSelections('test', [new Selection(3, 1000, 3, 1000)]); typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste4 = teste "ok'); - assert.equal(model.getLineContent(4), 'teste4 = teste "ok"'); + assert.strictEqual(model.getLineContent(4), 'teste4 = teste "ok"'); // Second gif viewModel.setSelections('test', [new Selection(4, 1000, 4, 1000)]); typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste \''); - assert.equal(model.getLineContent(5), 'teste \'\''); + assert.strictEqual(model.getLineContent(5), 'teste \'\''); viewModel.setSelections('test', [new Selection(5, 1000, 5, 1000)]); typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste "'); - assert.equal(model.getLineContent(6), 'teste ""'); + assert.strictEqual(model.getLineContent(6), 'teste ""'); viewModel.setSelections('test', [new Selection(6, 1000, 6, 1000)]); typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste\''); - assert.equal(model.getLineContent(7), 'teste\''); + assert.strictEqual(model.getLineContent(7), 'teste\''); viewModel.setSelections('test', [new Selection(7, 1000, 7, 1000)]); typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste"'); - assert.equal(model.getLineContent(8), 'teste"'); + assert.strictEqual(model.getLineContent(8), 'teste"'); }); mode.dispose(); }); @@ -5337,7 +5452,7 @@ suite('autoClosingPairs', () => { viewModel.replacePreviousChar('è', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), 'è'); + assert.strictEqual(model.getValue(), 'è'); }); mode.dispose(); }); @@ -5359,7 +5474,7 @@ suite('autoClosingPairs', () => { viewModel.replacePreviousChar('\'', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), '\'test\''); + assert.strictEqual(model.getValue(), '\'test\''); }); mode.dispose(); }); @@ -5376,16 +5491,16 @@ suite('autoClosingPairs', () => { viewModel.setSelections('test', [new Selection(1, 13, 1, 13)]); viewModel.type('\'', 'keyboard'); - assert.equal(model.getValue(), 'console.log(\'\');'); + assert.strictEqual(model.getValue(), 'console.log(\'\');'); viewModel.type('it', 'keyboard'); - assert.equal(model.getValue(), 'console.log(\'it\');'); + assert.strictEqual(model.getValue(), 'console.log(\'it\');'); viewModel.type('\\', 'keyboard'); - assert.equal(model.getValue(), 'console.log(\'it\\\');'); + assert.strictEqual(model.getValue(), 'console.log(\'it\\\');'); viewModel.type('\'', 'keyboard'); - assert.equal(model.getValue(), 'console.log(\'it\\\'\');'); + assert.strictEqual(model.getValue(), 'console.log(\'it\\\'\');'); }); mode.dispose(); }); @@ -5402,19 +5517,19 @@ suite('autoClosingPairs', () => { viewModel.setSelections('test', [new Selection(1, 1, 1, 1)]); viewModel.type('\\', 'keyboard'); - assert.equal(model.getValue(), '\\'); + assert.strictEqual(model.getValue(), '\\'); viewModel.type('(', 'keyboard'); - assert.equal(model.getValue(), '\\()'); + assert.strictEqual(model.getValue(), '\\()'); viewModel.type('abc', 'keyboard'); - assert.equal(model.getValue(), '\\(abc)'); + assert.strictEqual(model.getValue(), '\\(abc)'); viewModel.type('\\', 'keyboard'); - assert.equal(model.getValue(), '\\(abc\\)'); + assert.strictEqual(model.getValue(), '\\(abc\\)'); viewModel.type(')', 'keyboard'); - assert.equal(model.getValue(), '\\(abc\\)'); + assert.strictEqual(model.getValue(), '\\(abc\\)'); }); mode.dispose(); }); @@ -5439,7 +5554,7 @@ suite('autoClosingPairs', () => { viewModel.replacePreviousChar('`', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), '`hello\nworld'); + assert.strictEqual(model.getValue(), '`hello\nworld'); assertCursor(viewModel, new Selection(1, 2, 2, 2)); }); mode.dispose(); @@ -5462,14 +5577,14 @@ suite('autoClosingPairs', () => { viewModel.type('\'', 'keyboard'); viewModel.replacePreviousChar('\'', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), '\'\''); + assert.strictEqual(model.getValue(), '\'\''); // Typing one more ' + space viewModel.startComposition(); viewModel.type('\'', 'keyboard'); viewModel.replacePreviousChar('\'', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), '\'\''); + assert.strictEqual(model.getValue(), '\'\''); // Typing ' as a closing tag model.setValue('\'abc'); @@ -5479,7 +5594,7 @@ suite('autoClosingPairs', () => { viewModel.replacePreviousChar('\'', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), '\'abc\''); + assert.strictEqual(model.getValue(), '\'abc\''); // quotes before the newly added character are all paired. model.setValue('\'abc\'def '); @@ -5489,7 +5604,7 @@ suite('autoClosingPairs', () => { viewModel.replacePreviousChar('\'', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), '\'abc\'def \'\''); + assert.strictEqual(model.getValue(), '\'abc\'def \'\''); // No auto closing if there is non-whitespace character after the cursor model.setValue('abc'); @@ -5507,7 +5622,7 @@ suite('autoClosingPairs', () => { viewModel.replacePreviousChar('\'', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), 'abc\''); + assert.strictEqual(model.getValue(), 'abc\''); }); mode.dispose(); }); @@ -5527,7 +5642,7 @@ suite('autoClosingPairs', () => { viewModel.type('a', 'keyboard'); viewModel.replacePreviousChar('', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), '{}'); + assert.strictEqual(model.getValue(), '{}'); }); mode.dispose(); }); @@ -5549,7 +5664,7 @@ suite('autoClosingPairs', () => { // type a ` viewModel.type('`', 'keyboard'); - assert.equal(model.getValue(), 'var a = `asd`'); + assert.strictEqual(model.getValue(), 'var a = `asd`'); }); mode.dispose(); }); @@ -5577,14 +5692,14 @@ suite('autoClosingPairs', () => { new Selection(1, 12, 1, 13) ]); viewModel.type('"', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), 'var x = "hi";', 'assert1'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'var x = "hi";', 'assert1'); editor.setSelections([ new Selection(1, 9, 1, 10), new Selection(1, 12, 1, 13) ]); viewModel.type('\'', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), 'var x = \'hi\';', 'assert2'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'var x = \'hi\';', 'assert2'); }); model.dispose(); @@ -5610,7 +5725,7 @@ suite('autoClosingPairs', () => { // delete left CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), 'va a = )'); + assert.strictEqual(model.getValue(), 'va a = )'); }); model.dispose(); mode.dispose(); @@ -5657,20 +5772,20 @@ suite('Undo stops', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { viewModel.setSelections('test', [new Selection(1, 3, 1, 3)]); viewModel.type('first', 'keyboard'); - assert.equal(model.getLineContent(1), 'A first line'); + assert.strictEqual(model.getLineContent(1), 'A first line'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A fir line'); + assert.strictEqual(model.getLineContent(1), 'A fir line'); assertCursor(viewModel, new Selection(1, 6, 1, 6)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A first line'); + assert.strictEqual(model.getLineContent(1), 'A first line'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A line'); + assert.strictEqual(model.getLineContent(1), 'A line'); assertCursor(viewModel, new Selection(1, 3, 1, 3)); }); }); @@ -5686,20 +5801,20 @@ suite('Undo stops', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { viewModel.setSelections('test', [new Selection(1, 3, 1, 3)]); viewModel.type('first', 'keyboard'); - assert.equal(model.getLineContent(1), 'A first line'); + assert.strictEqual(model.getLineContent(1), 'A first line'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A firstine'); + assert.strictEqual(model.getLineContent(1), 'A firstine'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A first line'); + assert.strictEqual(model.getLineContent(1), 'A first line'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A line'); + assert.strictEqual(model.getLineContent(1), 'A line'); assertCursor(viewModel, new Selection(1, 3, 1, 3)); }); }); @@ -5721,19 +5836,19 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' line'); + assert.strictEqual(model.getLineContent(2), ' line'); assertCursor(viewModel, new Selection(2, 1, 2, 1)); viewModel.type('Second', 'keyboard'); - assert.equal(model.getLineContent(2), 'Second line'); + assert.strictEqual(model.getLineContent(2), 'Second line'); assertCursor(viewModel, new Selection(2, 7, 2, 7)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' line'); + assert.strictEqual(model.getLineContent(2), ' line'); assertCursor(viewModel, new Selection(2, 1, 2, 1)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another line'); + assert.strictEqual(model.getLineContent(2), 'Another line'); assertCursor(viewModel, new Selection(2, 8, 2, 8)); }); }); @@ -5755,7 +5870,7 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' line'); + assert.strictEqual(model.getLineContent(2), ' line'); assertCursor(viewModel, new Selection(2, 1, 2, 1)); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); @@ -5763,15 +5878,15 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(2), ''); assertCursor(viewModel, new Selection(2, 1, 2, 1)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' line'); + assert.strictEqual(model.getLineContent(2), ' line'); assertCursor(viewModel, new Selection(2, 1, 2, 1)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another line'); + assert.strictEqual(model.getLineContent(2), 'Another line'); assertCursor(viewModel, new Selection(2, 8, 2, 8)); }); }); @@ -5790,19 +5905,19 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another '); + assert.strictEqual(model.getLineContent(2), 'Another '); assertCursor(viewModel, new Selection(2, 9, 2, 9)); viewModel.type('text', 'keyboard'); - assert.equal(model.getLineContent(2), 'Another text'); + assert.strictEqual(model.getLineContent(2), 'Another text'); assertCursor(viewModel, new Selection(2, 13, 2, 13)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another '); + assert.strictEqual(model.getLineContent(2), 'Another '); assertCursor(viewModel, new Selection(2, 9, 2, 9)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another line'); + assert.strictEqual(model.getLineContent(2), 'Another line'); assertCursor(viewModel, new Selection(2, 9, 2, 9)); }); }); @@ -5821,7 +5936,7 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another '); + assert.strictEqual(model.getLineContent(2), 'Another '); assertCursor(viewModel, new Selection(2, 9, 2, 9)); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); @@ -5830,15 +5945,15 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'An'); + assert.strictEqual(model.getLineContent(2), 'An'); assertCursor(viewModel, new Selection(2, 3, 2, 3)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another '); + assert.strictEqual(model.getLineContent(2), 'Another '); assertCursor(viewModel, new Selection(2, 9, 2, 9)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another line'); + assert.strictEqual(model.getLineContent(2), 'Another line'); assertCursor(viewModel, new Selection(2, 9, 2, 9)); }); }); @@ -5854,19 +5969,19 @@ suite('Undo stops', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { viewModel.setSelections('test', [new Selection(1, 3, 1, 3)]); viewModel.type('first and interesting', 'keyboard'); - assert.equal(model.getLineContent(1), 'A first and interesting line'); + assert.strictEqual(model.getLineContent(1), 'A first and interesting line'); assertCursor(viewModel, new Selection(1, 24, 1, 24)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A first and line'); + assert.strictEqual(model.getLineContent(1), 'A first and line'); assertCursor(viewModel, new Selection(1, 12, 1, 12)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A first line'); + assert.strictEqual(model.getLineContent(1), 'A first line'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A line'); + assert.strictEqual(model.getLineContent(1), 'A line'); assertCursor(viewModel, new Selection(1, 3, 1, 3)); }); }); @@ -5882,15 +5997,15 @@ suite('Undo stops', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { viewModel.setSelections('test', [new Selection(1, 3, 1, 3)]); viewModel.type('first', 'keyboard'); - assert.equal(model.getValue(), 'A first line\nAnother line'); + assert.strictEqual(model.getValue(), 'A first line\nAnother line'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); model.pushEOL(EndOfLineSequence.CRLF); - assert.equal(model.getValue(), 'A first line\r\nAnother line'); + assert.strictEqual(model.getValue(), 'A first line\r\nAnother line'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), 'A line\nAnother line'); + assert.strictEqual(model.getValue(), 'A line\nAnother line'); assertCursor(viewModel, new Selection(1, 3, 1, 3)); }); }); @@ -5909,10 +6024,10 @@ suite('Undo stops', () => { new Selection(1, 7, 1, 12), ]); viewModel.type('no', 'keyboard'); - assert.equal(model.getValue(), 'hello no\nhello no'); + assert.strictEqual(model.getValue(), 'hello no\nhello no'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), 'hello world\nhello world'); + assert.strictEqual(model.getValue(), 'hello world\nhello world'); }); }); }); diff --git a/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts b/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts index 3c85cc22..55abff9f 100644 --- a/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts +++ b/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts @@ -484,11 +484,11 @@ function cursorEqual(viewModel: ViewModel, posLineNumber: number, posColumn: num } function positionEqual(position: Position, lineNumber: number, column: number) { - assert.deepEqual(position, new Position(lineNumber, column), 'position equal'); + assert.deepStrictEqual(position, new Position(lineNumber, column), 'position equal'); } function selectionEqual(selection: Selection, posLineNumber: number, posColumn: number, selLineNumber: number, selColumn: number) { - assert.deepEqual({ + assert.deepStrictEqual({ selectionStartLineNumber: selection.selectionStartLineNumber, selectionStartColumn: selection.selectionStartColumn, positionLineNumber: selection.positionLineNumber, diff --git a/src/vs/editor/test/browser/controller/textAreaState.test.ts b/src/vs/editor/test/browser/controller/textAreaState.test.ts index 163e9de5..4ae25e14 100644 --- a/src/vs/editor/test/browser/controller/textAreaState.test.ts +++ b/src/vs/editor/test/browser/controller/textAreaState.test.ts @@ -84,8 +84,8 @@ suite('TextAreaState', () => { let actual = TextAreaState.readFromTextArea(textArea); assertTextAreaState(actual, 'Hello world!', 1, 12); - assert.equal(actual.value, 'Hello world!'); - assert.equal(actual.selectionStart, 1); + assert.strictEqual(actual.value, 'Hello world!'); + assert.strictEqual(actual.selectionStart, 1); actual = actual.collapseSelection(); assertTextAreaState(actual, 'Hello world!', 12, 12); @@ -102,23 +102,23 @@ suite('TextAreaState', () => { let state = new TextAreaState('Hi world!', 2, 2, null, null); state.writeToTextArea('test', textArea, false); - assert.equal(textArea._value, 'Hi world!'); - assert.equal(textArea._selectionStart, 9); - assert.equal(textArea._selectionEnd, 9); + assert.strictEqual(textArea._value, 'Hi world!'); + assert.strictEqual(textArea._selectionStart, 9); + assert.strictEqual(textArea._selectionEnd, 9); state = new TextAreaState('Hi world!', 3, 3, null, null); state.writeToTextArea('test', textArea, false); - assert.equal(textArea._value, 'Hi world!'); - assert.equal(textArea._selectionStart, 9); - assert.equal(textArea._selectionEnd, 9); + assert.strictEqual(textArea._value, 'Hi world!'); + assert.strictEqual(textArea._selectionStart, 9); + assert.strictEqual(textArea._selectionEnd, 9); state = new TextAreaState('Hi world!', 0, 2, null, null); state.writeToTextArea('test', textArea, true); - assert.equal(textArea._value, 'Hi world!'); - assert.equal(textArea._selectionStart, 0); - assert.equal(textArea._selectionEnd, 2); + assert.strictEqual(textArea._value, 'Hi world!'); + assert.strictEqual(textArea._selectionStart, 0); + assert.strictEqual(textArea._selectionEnd, 2); textArea.dispose(); }); @@ -134,8 +134,8 @@ suite('TextAreaState', () => { let newState = TextAreaState.readFromTextArea(textArea); let actual = TextAreaState.deduceInput(prevState, newState, couldBeEmojiInput); - assert.equal(actual.text, expected); - assert.equal(actual.replaceCharCnt, expectedCharReplaceCnt); + assert.strictEqual(actual.text, expected); + assert.strictEqual(actual.replaceCharCnt, expectedCharReplaceCnt); textArea.dispose(); } diff --git a/src/vs/editor/test/browser/core/editorState.test.ts b/src/vs/editor/test/browser/core/editorState.test.ts index 1915af4f..32534c2b 100644 --- a/src/vs/editor/test/browser/core/editorState.test.ts +++ b/src/vs/editor/test/browser/core/editorState.test.ts @@ -29,7 +29,7 @@ suite('Editor Core - Editor State', () => { test('empty editor state should be valid', () => { let result = validate({}, {}); - assert.equal(result, true); + assert.strictEqual(result, true); }); test('different model URIs should be invalid', () => { @@ -38,7 +38,7 @@ suite('Editor Core - Editor State', () => { { model: { uri: URI.parse('http://test2') } } ); - assert.equal(result, false); + assert.strictEqual(result, false); }); test('different model versions should be invalid', () => { @@ -47,7 +47,7 @@ suite('Editor Core - Editor State', () => { { model: { version: 2 } } ); - assert.equal(result, false); + assert.strictEqual(result, false); }); test('different positions should be invalid', () => { @@ -56,7 +56,7 @@ suite('Editor Core - Editor State', () => { { position: new Position(2, 3) } ); - assert.equal(result, false); + assert.strictEqual(result, false); }); test('different selections should be invalid', () => { @@ -65,7 +65,7 @@ suite('Editor Core - Editor State', () => { { selection: new Selection(5, 2, 3, 4) } ); - assert.equal(result, false); + assert.strictEqual(result, false); }); test('different scroll positions should be invalid', () => { @@ -74,7 +74,7 @@ suite('Editor Core - Editor State', () => { { scroll: { left: 3, top: 2 } } ); - assert.equal(result, false); + assert.strictEqual(result, false); }); diff --git a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts index 9c8c6c6e..37055a85 100644 --- a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts +++ b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts @@ -60,12 +60,12 @@ suite('Decoration Render Options', () => { test('register and resolve decoration type', () => { let s = new TestCodeEditorServiceImpl(themeServiceMock); s.registerDecorationType('example', options); - assert.notEqual(s.resolveDecorationOptions('example', false), undefined); + assert.notStrictEqual(s.resolveDecorationOptions('example', false), undefined); }); test('remove decoration type', () => { let s = new TestCodeEditorServiceImpl(themeServiceMock); s.registerDecorationType('example', options); - assert.notEqual(s.resolveDecorationOptions('example', false), undefined); + assert.notStrictEqual(s.resolveDecorationOptions('example', false), undefined); s.removeDecorationType('example'); assert.throws(() => s.resolveDecorationOptions('example', false)); }); @@ -95,16 +95,16 @@ suite('Decoration Render Options', () => { })); const s = new TestCodeEditorServiceImpl(themeService, styleSheet); s.registerDecorationType('example', options); - assert.equal(readStyleSheet(styleSheet), '.monaco-editor .ced-example-0 {background-color:#ff0000;border-color:transparent;box-sizing: border-box;}'); + assert.strictEqual(readStyleSheet(styleSheet), '.monaco-editor .ced-example-0 {background-color:#ff0000;border-color:transparent;box-sizing: border-box;}'); themeService.setTheme(new TestColorTheme({ editorBackground: '#EE0000', editorBorder: '#00FFFF' })); - assert.equal(readStyleSheet(styleSheet), '.monaco-editor .ced-example-0 {background-color:#ee0000;border-color:#00ffff;box-sizing: border-box;}'); + assert.strictEqual(readStyleSheet(styleSheet), '.monaco-editor .ced-example-0 {background-color:#ee0000;border-color:#00ffff;box-sizing: border-box;}'); s.removeDecorationType('example'); - assert.equal(readStyleSheet(styleSheet), ''); + assert.strictEqual(readStyleSheet(styleSheet), ''); }); test('theme overrides', () => { @@ -134,10 +134,10 @@ suite('Decoration Render Options', () => { '.vs.monaco-editor .ced-example-1 {color:#FF00FF !important;}', '.monaco-editor .ced-example-1 {color:#ff0000 !important;}' ].join('\n'); - assert.equal(readStyleSheet(styleSheet), expected); + assert.strictEqual(readStyleSheet(styleSheet), expected); s.removeDecorationType('example'); - assert.equal(readStyleSheet(styleSheet), ''); + assert.strictEqual(readStyleSheet(styleSheet), ''); }); test('css properties, gutterIconPaths', () => { diff --git a/src/vs/editor/test/browser/testCodeEditor.ts b/src/vs/editor/test/browser/testCodeEditor.ts index cc39f304..da4aac21 100644 --- a/src/vs/editor/test/browser/testCodeEditor.ts +++ b/src/vs/editor/test/browser/testCodeEditor.ts @@ -31,10 +31,10 @@ export interface ITestCodeEditor extends IActiveCodeEditor { registerAndInstantiateContribution(id: string, ctor: new (editor: ICodeEditor, ...services: Services) => T): T; } -class TestCodeEditor extends CodeEditorWidget implements ICodeEditor { +export class TestCodeEditor extends CodeEditorWidget implements ICodeEditor { //#region testing overrides - protected _createConfiguration(options: IEditorConstructionOptions): IConfiguration { + protected _createConfiguration(options: Readonly): IConfiguration { return new TestConfiguration(options); } protected _createView(viewModel: ViewModel): [View, boolean] { @@ -52,6 +52,9 @@ class TestCodeEditor extends CodeEditorWidget implements ICodeEditor { this._contributions[id] = r; return r; } +} + +class TestCodeEditorWithAutoModelDisposal extends TestCodeEditor { public dispose() { super.dispose(); if (this._modelData) { @@ -144,7 +147,7 @@ export function createTestCodeEditor(options: TestCodeEditorCreationOptions): IT contributions: [] }; const editor = instantiationService.createInstance( - TestCodeEditor, + TestCodeEditorWithAutoModelDisposal, new TestEditorDomElement(), options, codeEditorWidgetOptions diff --git a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts index cde86790..81aed990 100644 --- a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts +++ b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts @@ -85,7 +85,7 @@ suite('MinimapCharRenderer', () => { actual[i] = imageData.data[i]; } - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ 0x2D, 0x2D, 0x2D, 0xFF, 0xAC, 0xAC, 0xAC, 0xFF, 0xC6, 0xC6, 0xC6, 0xFF, 0xC8, 0xC8, 0xC8, 0xFF, 0xC0, 0xC0, 0xC0, 0xFF, 0xCB, 0xCB, 0xCB, 0xFF, @@ -115,7 +115,7 @@ suite('MinimapCharRenderer', () => { actual[i] = imageData.data[i]; } - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ 0xCB, 0xCB, 0xCB, 0xFF, 0x81, 0x81, 0x81, 0xFF, ]); diff --git a/src/vs/editor/test/browser/view/viewLayer.test.ts b/src/vs/editor/test/browser/view/viewLayer.test.ts index bffb2f8b..eba81ff7 100644 --- a/src/vs/editor/test/browser/view/viewLayer.test.ts +++ b/src/vs/editor/test/browser/view/viewLayer.test.ts @@ -36,7 +36,7 @@ function assertState(col: RenderedLinesCollection, state: ILinesCollec actualState.lines.push(col.getLine(lineNumber).id); actualState.pinged.push(col.getLine(lineNumber)._pinged); } - assert.deepEqual(actualState, state); + assert.deepStrictEqual(actualState, state); } suite('RenderedLinesCollection onLinesDeleted', () => { @@ -54,7 +54,7 @@ suite('RenderedLinesCollection onLinesDeleted', () => { if (actualDeleted1) { actualDeleted = actualDeleted1.map(line => line.id); } - assert.deepEqual(actualDeleted, expectedDeleted); + assert.deepStrictEqual(actualDeleted, expectedDeleted); assertState(col, expectedState); } @@ -325,7 +325,7 @@ suite('RenderedLinesCollection onLineChanged', () => { new TestLine('old9') ]); let actualPinged = col.onLinesChanged(changedLineNumber, changedLineNumber); - assert.deepEqual(actualPinged, expectedPinged); + assert.deepStrictEqual(actualPinged, expectedPinged); assertState(col, expectedState); } @@ -410,7 +410,7 @@ suite('RenderedLinesCollection onLinesInserted', () => { if (actualDeleted1) { actualDeleted = actualDeleted1.map(line => line.id); } - assert.deepEqual(actualDeleted, expectedDeleted); + assert.deepStrictEqual(actualDeleted, expectedDeleted); assertState(col, expectedState); } @@ -682,7 +682,7 @@ suite('RenderedLinesCollection onTokensChanged', () => { new TestLine('old9') ]); let actualPinged = col.onTokensChanged([{ fromLineNumber: changedFromLineNumber, toLineNumber: changedToLineNumber }]); - assert.deepEqual(actualPinged, expectedPinged); + assert.deepStrictEqual(actualPinged, expectedPinged); assertState(col, expectedState); } diff --git a/src/vs/editor/test/common/config/commonEditorConfig.test.ts b/src/vs/editor/test/common/config/commonEditorConfig.test.ts index d0edc8f4..1faca7a1 100644 --- a/src/vs/editor/test/common/config/commonEditorConfig.test.ts +++ b/src/vs/editor/test/common/config/commonEditorConfig.test.ts @@ -16,40 +16,40 @@ suite('Common Editor Config', () => { const zoom = EditorZoom; zoom.setZoomLevel(0); - assert.equal(zoom.getZoomLevel(), 0); + assert.strictEqual(zoom.getZoomLevel(), 0); zoom.setZoomLevel(-0); - assert.equal(zoom.getZoomLevel(), 0); + assert.strictEqual(zoom.getZoomLevel(), 0); zoom.setZoomLevel(5); - assert.equal(zoom.getZoomLevel(), 5); + assert.strictEqual(zoom.getZoomLevel(), 5); zoom.setZoomLevel(-1); - assert.equal(zoom.getZoomLevel(), -1); + assert.strictEqual(zoom.getZoomLevel(), -1); zoom.setZoomLevel(9); - assert.equal(zoom.getZoomLevel(), 9); + assert.strictEqual(zoom.getZoomLevel(), 9); zoom.setZoomLevel(-9); - assert.equal(zoom.getZoomLevel(), -5); + assert.strictEqual(zoom.getZoomLevel(), -5); zoom.setZoomLevel(20); - assert.equal(zoom.getZoomLevel(), 20); + assert.strictEqual(zoom.getZoomLevel(), 20); zoom.setZoomLevel(-10); - assert.equal(zoom.getZoomLevel(), -5); + assert.strictEqual(zoom.getZoomLevel(), -5); zoom.setZoomLevel(9.1); - assert.equal(zoom.getZoomLevel(), 9.1); + assert.strictEqual(zoom.getZoomLevel(), 9.1); zoom.setZoomLevel(-9.1); - assert.equal(zoom.getZoomLevel(), -5); + assert.strictEqual(zoom.getZoomLevel(), -5); zoom.setZoomLevel(Infinity); - assert.equal(zoom.getZoomLevel(), 20); + assert.strictEqual(zoom.getZoomLevel(), 20); zoom.setZoomLevel(Number.NEGATIVE_INFINITY); - assert.equal(zoom.getZoomLevel(), -5); + assert.strictEqual(zoom.getZoomLevel(), -5); }); class TestWrappingConfiguration extends TestConfiguration { @@ -69,8 +69,8 @@ suite('Common Editor Config', () => { function assertWrapping(config: TestConfiguration, isViewportWrapping: boolean, wrappingColumn: number): void { const options = config.options; const wrappingInfo = options.get(EditorOption.wrappingInfo); - assert.equal(wrappingInfo.isViewportWrapping, isViewportWrapping); - assert.equal(wrappingInfo.wrappingColumn, wrappingColumn); + assert.strictEqual(wrappingInfo.isViewportWrapping, isViewportWrapping); + assert.strictEqual(wrappingInfo.wrappingColumn, wrappingColumn); } test('wordWrap default', () => { @@ -186,26 +186,26 @@ suite('Common Editor Config', () => { }); let config = new TestConfiguration({ hover: hoverOptions }); - assert.equal(config.options.get(EditorOption.hover).enabled, true); + assert.strictEqual(config.options.get(EditorOption.hover).enabled, true); config.updateOptions({ hover: { enabled: false } }); - assert.equal(config.options.get(EditorOption.hover).enabled, false); + assert.strictEqual(config.options.get(EditorOption.hover).enabled, false); }); test('does not emit event when nothing changes', () => { const config = new TestConfiguration({ glyphMargin: true, roundedSelection: false }); let event: ConfigurationChangedEvent | null = null; config.onDidChange(e => event = e); - assert.equal(config.options.get(EditorOption.glyphMargin), true); + assert.strictEqual(config.options.get(EditorOption.glyphMargin), true); config.updateOptions({ glyphMargin: true }); config.updateOptions({ roundedSelection: false }); - assert.equal(event, null); + assert.strictEqual(event, null); }); test('issue #94931: Unable to open source file', () => { const config = new TestConfiguration({ quickSuggestions: null! }); const actual = >>config.options.get(EditorOption.quickSuggestions); - assert.deepEqual(actual, { + assert.deepStrictEqual(actual, { other: true, comments: false, strings: false @@ -216,7 +216,7 @@ suite('Common Editor Config', () => { const config = new TestConfiguration({ quickSuggestions: null! }); config.updateOptions({ quickSuggestions: { strings: true } }); const actual = >>config.options.get(EditorOption.quickSuggestions); - assert.deepEqual(actual, { + assert.deepStrictEqual(actual, { other: true, comments: false, strings: true diff --git a/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts b/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts index e7cd6b4b..e0704228 100644 --- a/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts +++ b/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts @@ -8,41 +8,41 @@ import { CursorColumns } from 'vs/editor/common/controller/cursorCommon'; suite('CursorMove', () => { test('nextRenderTabStop', () => { - assert.equal(CursorColumns.nextRenderTabStop(0, 4), 4); - assert.equal(CursorColumns.nextRenderTabStop(1, 4), 4); - assert.equal(CursorColumns.nextRenderTabStop(2, 4), 4); - assert.equal(CursorColumns.nextRenderTabStop(3, 4), 4); - assert.equal(CursorColumns.nextRenderTabStop(4, 4), 8); - assert.equal(CursorColumns.nextRenderTabStop(5, 4), 8); - assert.equal(CursorColumns.nextRenderTabStop(6, 4), 8); - assert.equal(CursorColumns.nextRenderTabStop(7, 4), 8); - assert.equal(CursorColumns.nextRenderTabStop(8, 4), 12); + assert.strictEqual(CursorColumns.nextRenderTabStop(0, 4), 4); + assert.strictEqual(CursorColumns.nextRenderTabStop(1, 4), 4); + assert.strictEqual(CursorColumns.nextRenderTabStop(2, 4), 4); + assert.strictEqual(CursorColumns.nextRenderTabStop(3, 4), 4); + assert.strictEqual(CursorColumns.nextRenderTabStop(4, 4), 8); + assert.strictEqual(CursorColumns.nextRenderTabStop(5, 4), 8); + assert.strictEqual(CursorColumns.nextRenderTabStop(6, 4), 8); + assert.strictEqual(CursorColumns.nextRenderTabStop(7, 4), 8); + assert.strictEqual(CursorColumns.nextRenderTabStop(8, 4), 12); - assert.equal(CursorColumns.nextRenderTabStop(0, 2), 2); - assert.equal(CursorColumns.nextRenderTabStop(1, 2), 2); - assert.equal(CursorColumns.nextRenderTabStop(2, 2), 4); - assert.equal(CursorColumns.nextRenderTabStop(3, 2), 4); - assert.equal(CursorColumns.nextRenderTabStop(4, 2), 6); - assert.equal(CursorColumns.nextRenderTabStop(5, 2), 6); - assert.equal(CursorColumns.nextRenderTabStop(6, 2), 8); - assert.equal(CursorColumns.nextRenderTabStop(7, 2), 8); - assert.equal(CursorColumns.nextRenderTabStop(8, 2), 10); + assert.strictEqual(CursorColumns.nextRenderTabStop(0, 2), 2); + assert.strictEqual(CursorColumns.nextRenderTabStop(1, 2), 2); + assert.strictEqual(CursorColumns.nextRenderTabStop(2, 2), 4); + assert.strictEqual(CursorColumns.nextRenderTabStop(3, 2), 4); + assert.strictEqual(CursorColumns.nextRenderTabStop(4, 2), 6); + assert.strictEqual(CursorColumns.nextRenderTabStop(5, 2), 6); + assert.strictEqual(CursorColumns.nextRenderTabStop(6, 2), 8); + assert.strictEqual(CursorColumns.nextRenderTabStop(7, 2), 8); + assert.strictEqual(CursorColumns.nextRenderTabStop(8, 2), 10); - assert.equal(CursorColumns.nextRenderTabStop(0, 1), 1); - assert.equal(CursorColumns.nextRenderTabStop(1, 1), 2); - assert.equal(CursorColumns.nextRenderTabStop(2, 1), 3); - assert.equal(CursorColumns.nextRenderTabStop(3, 1), 4); - assert.equal(CursorColumns.nextRenderTabStop(4, 1), 5); - assert.equal(CursorColumns.nextRenderTabStop(5, 1), 6); - assert.equal(CursorColumns.nextRenderTabStop(6, 1), 7); - assert.equal(CursorColumns.nextRenderTabStop(7, 1), 8); - assert.equal(CursorColumns.nextRenderTabStop(8, 1), 9); + assert.strictEqual(CursorColumns.nextRenderTabStop(0, 1), 1); + assert.strictEqual(CursorColumns.nextRenderTabStop(1, 1), 2); + assert.strictEqual(CursorColumns.nextRenderTabStop(2, 1), 3); + assert.strictEqual(CursorColumns.nextRenderTabStop(3, 1), 4); + assert.strictEqual(CursorColumns.nextRenderTabStop(4, 1), 5); + assert.strictEqual(CursorColumns.nextRenderTabStop(5, 1), 6); + assert.strictEqual(CursorColumns.nextRenderTabStop(6, 1), 7); + assert.strictEqual(CursorColumns.nextRenderTabStop(7, 1), 8); + assert.strictEqual(CursorColumns.nextRenderTabStop(8, 1), 9); }); test('visibleColumnFromColumn', () => { function testVisibleColumnFromColumn(text: string, tabSize: number, column: number, expected: number): void { - assert.equal(CursorColumns.visibleColumnFromColumn(text, column, tabSize), expected); + assert.strictEqual(CursorColumns.visibleColumnFromColumn(text, column, tabSize), expected); } testVisibleColumnFromColumn('\t\tvar x = 3;', 4, 1, 0); @@ -101,7 +101,7 @@ suite('CursorMove', () => { test('columnFromVisibleColumn', () => { function testColumnFromVisibleColumn(text: string, tabSize: number, visibleColumn: number, expected: number): void { - assert.equal(CursorColumns.columnFromVisibleColumn(text, visibleColumn, tabSize), expected); + assert.strictEqual(CursorColumns.columnFromVisibleColumn(text, visibleColumn, tabSize), expected); } // testColumnFromVisibleColumn('\t\tvar x = 3;', 4, 0, 1); @@ -177,7 +177,7 @@ suite('CursorMove', () => { test('toStatusbarColumn', () => { function t(text: string, tabSize: number, column: number, expected: number): void { - assert.equal(CursorColumns.toStatusbarColumn(text, column, tabSize), expected, `<>`); + assert.strictEqual(CursorColumns.toStatusbarColumn(text, column, tabSize), expected, `<>`); } t(' spaces', 4, 1, 1); diff --git a/src/vs/editor/test/common/core/characterClassifier.test.ts b/src/vs/editor/test/common/core/characterClassifier.test.ts index de9effc7..9d3bf750 100644 --- a/src/vs/editor/test/common/core/characterClassifier.test.ts +++ b/src/vs/editor/test/common/core/characterClassifier.test.ts @@ -11,27 +11,27 @@ suite('CharacterClassifier', () => { test('works', () => { let classifier = new CharacterClassifier(0); - assert.equal(classifier.get(-1), 0); - assert.equal(classifier.get(0), 0); - assert.equal(classifier.get(CharCode.a), 0); - assert.equal(classifier.get(CharCode.b), 0); - assert.equal(classifier.get(CharCode.z), 0); - assert.equal(classifier.get(255), 0); - assert.equal(classifier.get(1000), 0); - assert.equal(classifier.get(2000), 0); + assert.strictEqual(classifier.get(-1), 0); + assert.strictEqual(classifier.get(0), 0); + assert.strictEqual(classifier.get(CharCode.a), 0); + assert.strictEqual(classifier.get(CharCode.b), 0); + assert.strictEqual(classifier.get(CharCode.z), 0); + assert.strictEqual(classifier.get(255), 0); + assert.strictEqual(classifier.get(1000), 0); + assert.strictEqual(classifier.get(2000), 0); classifier.set(CharCode.a, 1); classifier.set(CharCode.z, 2); classifier.set(1000, 3); - assert.equal(classifier.get(-1), 0); - assert.equal(classifier.get(0), 0); - assert.equal(classifier.get(CharCode.a), 1); - assert.equal(classifier.get(CharCode.b), 0); - assert.equal(classifier.get(CharCode.z), 2); - assert.equal(classifier.get(255), 0); - assert.equal(classifier.get(1000), 3); - assert.equal(classifier.get(2000), 0); + assert.strictEqual(classifier.get(-1), 0); + assert.strictEqual(classifier.get(0), 0); + assert.strictEqual(classifier.get(CharCode.a), 1); + assert.strictEqual(classifier.get(CharCode.b), 0); + assert.strictEqual(classifier.get(CharCode.z), 2); + assert.strictEqual(classifier.get(255), 0); + assert.strictEqual(classifier.get(1000), 3); + assert.strictEqual(classifier.get(2000), 0); }); -}); \ No newline at end of file +}); diff --git a/src/vs/editor/test/common/core/lineTokens.test.ts b/src/vs/editor/test/common/core/lineTokens.test.ts index d3a9924f..2ffff0c7 100644 --- a/src/vs/editor/test/common/core/lineTokens.test.ts +++ b/src/vs/editor/test/common/core/lineTokens.test.ts @@ -45,64 +45,64 @@ suite('LineTokens', () => { test('basics', () => { const lineTokens = createTestLineTokens(); - assert.equal(lineTokens.getLineContent(), 'Hello world, this is a lovely day'); - assert.equal(lineTokens.getLineContent().length, 33); - assert.equal(lineTokens.getCount(), 7); + assert.strictEqual(lineTokens.getLineContent(), 'Hello world, this is a lovely day'); + assert.strictEqual(lineTokens.getLineContent().length, 33); + assert.strictEqual(lineTokens.getCount(), 7); - assert.equal(lineTokens.getStartOffset(0), 0); - assert.equal(lineTokens.getEndOffset(0), 6); - assert.equal(lineTokens.getStartOffset(1), 6); - assert.equal(lineTokens.getEndOffset(1), 13); - assert.equal(lineTokens.getStartOffset(2), 13); - assert.equal(lineTokens.getEndOffset(2), 18); - assert.equal(lineTokens.getStartOffset(3), 18); - assert.equal(lineTokens.getEndOffset(3), 21); - assert.equal(lineTokens.getStartOffset(4), 21); - assert.equal(lineTokens.getEndOffset(4), 23); - assert.equal(lineTokens.getStartOffset(5), 23); - assert.equal(lineTokens.getEndOffset(5), 30); - assert.equal(lineTokens.getStartOffset(6), 30); - assert.equal(lineTokens.getEndOffset(6), 33); + assert.strictEqual(lineTokens.getStartOffset(0), 0); + assert.strictEqual(lineTokens.getEndOffset(0), 6); + assert.strictEqual(lineTokens.getStartOffset(1), 6); + assert.strictEqual(lineTokens.getEndOffset(1), 13); + assert.strictEqual(lineTokens.getStartOffset(2), 13); + assert.strictEqual(lineTokens.getEndOffset(2), 18); + assert.strictEqual(lineTokens.getStartOffset(3), 18); + assert.strictEqual(lineTokens.getEndOffset(3), 21); + assert.strictEqual(lineTokens.getStartOffset(4), 21); + assert.strictEqual(lineTokens.getEndOffset(4), 23); + assert.strictEqual(lineTokens.getStartOffset(5), 23); + assert.strictEqual(lineTokens.getEndOffset(5), 30); + assert.strictEqual(lineTokens.getStartOffset(6), 30); + assert.strictEqual(lineTokens.getEndOffset(6), 33); }); test('findToken', () => { const lineTokens = createTestLineTokens(); - assert.equal(lineTokens.findTokenIndexAtOffset(0), 0); - assert.equal(lineTokens.findTokenIndexAtOffset(1), 0); - assert.equal(lineTokens.findTokenIndexAtOffset(2), 0); - assert.equal(lineTokens.findTokenIndexAtOffset(3), 0); - assert.equal(lineTokens.findTokenIndexAtOffset(4), 0); - assert.equal(lineTokens.findTokenIndexAtOffset(5), 0); - assert.equal(lineTokens.findTokenIndexAtOffset(6), 1); - assert.equal(lineTokens.findTokenIndexAtOffset(7), 1); - assert.equal(lineTokens.findTokenIndexAtOffset(8), 1); - assert.equal(lineTokens.findTokenIndexAtOffset(9), 1); - assert.equal(lineTokens.findTokenIndexAtOffset(10), 1); - assert.equal(lineTokens.findTokenIndexAtOffset(11), 1); - assert.equal(lineTokens.findTokenIndexAtOffset(12), 1); - assert.equal(lineTokens.findTokenIndexAtOffset(13), 2); - assert.equal(lineTokens.findTokenIndexAtOffset(14), 2); - assert.equal(lineTokens.findTokenIndexAtOffset(15), 2); - assert.equal(lineTokens.findTokenIndexAtOffset(16), 2); - assert.equal(lineTokens.findTokenIndexAtOffset(17), 2); - assert.equal(lineTokens.findTokenIndexAtOffset(18), 3); - assert.equal(lineTokens.findTokenIndexAtOffset(19), 3); - assert.equal(lineTokens.findTokenIndexAtOffset(20), 3); - assert.equal(lineTokens.findTokenIndexAtOffset(21), 4); - assert.equal(lineTokens.findTokenIndexAtOffset(22), 4); - assert.equal(lineTokens.findTokenIndexAtOffset(23), 5); - assert.equal(lineTokens.findTokenIndexAtOffset(24), 5); - assert.equal(lineTokens.findTokenIndexAtOffset(25), 5); - assert.equal(lineTokens.findTokenIndexAtOffset(26), 5); - assert.equal(lineTokens.findTokenIndexAtOffset(27), 5); - assert.equal(lineTokens.findTokenIndexAtOffset(28), 5); - assert.equal(lineTokens.findTokenIndexAtOffset(29), 5); - assert.equal(lineTokens.findTokenIndexAtOffset(30), 6); - assert.equal(lineTokens.findTokenIndexAtOffset(31), 6); - assert.equal(lineTokens.findTokenIndexAtOffset(32), 6); - assert.equal(lineTokens.findTokenIndexAtOffset(33), 6); - assert.equal(lineTokens.findTokenIndexAtOffset(34), 6); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(0), 0); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(1), 0); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(2), 0); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(3), 0); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(4), 0); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(5), 0); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(6), 1); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(7), 1); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(8), 1); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(9), 1); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(10), 1); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(11), 1); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(12), 1); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(13), 2); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(14), 2); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(15), 2); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(16), 2); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(17), 2); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(18), 3); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(19), 3); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(20), 3); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(21), 4); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(22), 4); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(23), 5); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(24), 5); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(25), 5); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(26), 5); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(27), 5); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(28), 5); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(29), 5); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(30), 6); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(31), 6); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(32), 6); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(33), 6); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(34), 6); }); interface ITestViewLineToken { @@ -118,7 +118,7 @@ suite('LineTokens', () => { foreground: _actual.getForeground(i) }; } - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } test('inflate', () => { diff --git a/src/vs/editor/test/common/core/range.test.ts b/src/vs/editor/test/common/core/range.test.ts index 5420ae45..26415e8c 100644 --- a/src/vs/editor/test/common/core/range.test.ts +++ b/src/vs/editor/test/common/core/range.test.ts @@ -9,47 +9,47 @@ import { Range } from 'vs/editor/common/core/range'; suite('Editor Core - Range', () => { test('empty range', () => { let s = new Range(1, 1, 1, 1); - assert.equal(s.startLineNumber, 1); - assert.equal(s.startColumn, 1); - assert.equal(s.endLineNumber, 1); - assert.equal(s.endColumn, 1); - assert.equal(s.isEmpty(), true); + assert.strictEqual(s.startLineNumber, 1); + assert.strictEqual(s.startColumn, 1); + assert.strictEqual(s.endLineNumber, 1); + assert.strictEqual(s.endColumn, 1); + assert.strictEqual(s.isEmpty(), true); }); test('swap start and stop same line', () => { let s = new Range(1, 2, 1, 1); - assert.equal(s.startLineNumber, 1); - assert.equal(s.startColumn, 1); - assert.equal(s.endLineNumber, 1); - assert.equal(s.endColumn, 2); - assert.equal(s.isEmpty(), false); + assert.strictEqual(s.startLineNumber, 1); + assert.strictEqual(s.startColumn, 1); + assert.strictEqual(s.endLineNumber, 1); + assert.strictEqual(s.endColumn, 2); + assert.strictEqual(s.isEmpty(), false); }); test('swap start and stop', () => { let s = new Range(2, 1, 1, 2); - assert.equal(s.startLineNumber, 1); - assert.equal(s.startColumn, 2); - assert.equal(s.endLineNumber, 2); - assert.equal(s.endColumn, 1); - assert.equal(s.isEmpty(), false); + assert.strictEqual(s.startLineNumber, 1); + assert.strictEqual(s.startColumn, 2); + assert.strictEqual(s.endLineNumber, 2); + assert.strictEqual(s.endColumn, 1); + assert.strictEqual(s.isEmpty(), false); }); test('no swap same line', () => { let s = new Range(1, 1, 1, 2); - assert.equal(s.startLineNumber, 1); - assert.equal(s.startColumn, 1); - assert.equal(s.endLineNumber, 1); - assert.equal(s.endColumn, 2); - assert.equal(s.isEmpty(), false); + assert.strictEqual(s.startLineNumber, 1); + assert.strictEqual(s.startColumn, 1); + assert.strictEqual(s.endLineNumber, 1); + assert.strictEqual(s.endColumn, 2); + assert.strictEqual(s.isEmpty(), false); }); test('no swap', () => { let s = new Range(1, 1, 2, 1); - assert.equal(s.startLineNumber, 1); - assert.equal(s.startColumn, 1); - assert.equal(s.endLineNumber, 2); - assert.equal(s.endColumn, 1); - assert.equal(s.isEmpty(), false); + assert.strictEqual(s.startLineNumber, 1); + assert.strictEqual(s.startColumn, 1); + assert.strictEqual(s.endLineNumber, 2); + assert.strictEqual(s.endColumn, 1); + assert.strictEqual(s.isEmpty(), false); }); test('compareRangesUsingEnds', () => { @@ -93,36 +93,36 @@ suite('Editor Core - Range', () => { }); test('containsPosition', () => { - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(1, 3)), false); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(2, 1)), false); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(2, 2)), true); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(2, 3)), true); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(3, 1)), true); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(5, 9)), true); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(5, 10)), true); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(5, 11)), false); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(6, 1)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(1, 3)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(2, 1)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(2, 2)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(2, 3)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(3, 1)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(5, 9)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(5, 10)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(5, 11)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(6, 1)), false); }); test('containsRange', () => { - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(1, 3, 2, 2)), false); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(2, 1, 2, 2)), false); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(2, 2, 5, 11)), false); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(2, 2, 6, 1)), false); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(5, 9, 6, 1)), false); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(5, 10, 6, 1)), false); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(2, 2, 5, 10)), true); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(2, 3, 5, 9)), true); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(3, 100, 4, 100)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(1, 3, 2, 2)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(2, 1, 2, 2)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(2, 2, 5, 11)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(2, 2, 6, 1)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(5, 9, 6, 1)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(5, 10, 6, 1)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(2, 2, 5, 10)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(2, 3, 5, 9)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(3, 100, 4, 100)), true); }); test('areIntersecting', () => { - assert.equal(Range.areIntersecting(new Range(2, 2, 3, 2), new Range(4, 2, 5, 2)), false); - assert.equal(Range.areIntersecting(new Range(4, 2, 5, 2), new Range(2, 2, 3, 2)), false); - assert.equal(Range.areIntersecting(new Range(4, 2, 5, 2), new Range(5, 2, 6, 2)), false); - assert.equal(Range.areIntersecting(new Range(5, 2, 6, 2), new Range(4, 2, 5, 2)), false); - assert.equal(Range.areIntersecting(new Range(2, 2, 2, 7), new Range(2, 4, 2, 6)), true); - assert.equal(Range.areIntersecting(new Range(2, 2, 2, 7), new Range(2, 4, 2, 9)), true); - assert.equal(Range.areIntersecting(new Range(2, 4, 2, 9), new Range(2, 2, 2, 7)), true); + assert.strictEqual(Range.areIntersecting(new Range(2, 2, 3, 2), new Range(4, 2, 5, 2)), false); + assert.strictEqual(Range.areIntersecting(new Range(4, 2, 5, 2), new Range(2, 2, 3, 2)), false); + assert.strictEqual(Range.areIntersecting(new Range(4, 2, 5, 2), new Range(5, 2, 6, 2)), false); + assert.strictEqual(Range.areIntersecting(new Range(5, 2, 6, 2), new Range(4, 2, 5, 2)), false); + assert.strictEqual(Range.areIntersecting(new Range(2, 2, 2, 7), new Range(2, 4, 2, 6)), true); + assert.strictEqual(Range.areIntersecting(new Range(2, 2, 2, 7), new Range(2, 4, 2, 9)), true); + assert.strictEqual(Range.areIntersecting(new Range(2, 4, 2, 9), new Range(2, 2, 2, 7)), true); }); }); diff --git a/src/vs/editor/test/common/diff/diffComputer.test.ts b/src/vs/editor/test/common/diff/diffComputer.test.ts index 9361e05d..32dd4bce 100644 --- a/src/vs/editor/test/common/diff/diffComputer.test.ts +++ b/src/vs/editor/test/common/diff/diffComputer.test.ts @@ -64,7 +64,7 @@ function assertDiff(originalLines: string[], modifiedLines: string[], expectedCh for (let i = 0; i < changes.length; i++) { extracted.push(extractLineChangeRepresentation(changes[i], (i < expectedChanges.length ? expectedChanges[i] : null))); } - assert.deepEqual(extracted, expectedChanges); + assert.deepStrictEqual(extracted, expectedChanges); } function createLineDeletion(startLineNumber: number, endLineNumber: number, modifiedLineNumber: number): ILineChange { @@ -462,6 +462,13 @@ suite('Editor Diff - DiffComputer', () => { assertDiff(original, modified, expected, true, false, true); }); + test('empty diff 5', () => { + let original = ['']; + let modified = ['']; + let expected: ILineChange[] = []; + assertDiff(original, modified, expected, true, false, true); + }); + test('pretty diff 1', () => { let original = [ 'suite(function () {', diff --git a/src/vs/editor/test/common/mocks/testConfiguration.ts b/src/vs/editor/test/common/mocks/testConfiguration.ts index a30fbc23..ec94f220 100644 --- a/src/vs/editor/test/common/mocks/testConfiguration.ts +++ b/src/vs/editor/test/common/mocks/testConfiguration.ts @@ -30,6 +30,7 @@ export class TestConfiguration extends CommonEditorConfiguration { protected readConfiguration(styling: BareFontInfo): FontInfo { return new FontInfo({ zoomLevel: 0, + pixelRatio: 1, fontFamily: 'mockFont', fontWeight: 'normal', fontSize: 14, diff --git a/src/vs/editor/test/common/model/benchmark/benchmarkUtils.ts b/src/vs/editor/test/common/model/benchmark/benchmarkUtils.ts index 79d4e1e4..357d3bb5 100644 --- a/src/vs/editor/test/common/model/benchmark/benchmarkUtils.ts +++ b/src/vs/editor/test/common/model/benchmark/benchmarkUtils.ts @@ -58,7 +58,7 @@ export class BenchmarkSuite { let timeDiffTotal = 0; for (let j = 0; j < this.iterations; j++) { let factory = benchmark.buildBuffer(builder); - let buffer = factory.create(DefaultEndOfLine.LF); + let buffer = factory.create(DefaultEndOfLine.LF).textBuffer; benchmark.preCycle(buffer); let start = process.hrtime(); benchmark.fn(buffer); diff --git a/src/vs/editor/test/common/model/editableTextModel.test.ts b/src/vs/editor/test/common/model/editableTextModel.test.ts index 67c8a119..c0e4be80 100644 --- a/src/vs/editor/test/common/model/editableTextModel.test.ts +++ b/src/vs/editor/test/common/model/editableTextModel.test.ts @@ -22,10 +22,10 @@ suite('EditorModel - EditableTextModel.applyEdits updates mightContainRTL', () = let model = createEditableTextModelFromString(original.join('\n')); model.setEOL(EndOfLineSequence.LF); - assert.equal(model.mightContainRTL(), before); + assert.strictEqual(model.mightContainRTL(), before); model.applyEdits(edits); - assert.equal(model.mightContainRTL(), after); + assert.strictEqual(model.mightContainRTL(), after); model.dispose(); } @@ -68,10 +68,10 @@ suite('EditorModel - EditableTextModel.applyEdits updates mightContainNonBasicAS let model = createEditableTextModelFromString(original.join('\n')); model.setEOL(EndOfLineSequence.LF); - assert.equal(model.mightContainNonBasicASCII(), before); + assert.strictEqual(model.mightContainNonBasicASCII(), before); model.applyEdits(edits); - assert.equal(model.mightContainNonBasicASCII(), after); + assert.strictEqual(model.mightContainNonBasicASCII(), after); model.dispose(); } @@ -1043,7 +1043,7 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { test('issue #1580: Changes in line endings are not correctly reflected in the extension host, leading to invalid offsets sent to external refactoring tools', () => { let model = createEditableTextModelFromString('Hello\nWorld!'); - assert.equal(model.getEOL(), '\n'); + assert.strictEqual(model.getEOL(), '\n'); let mirrorModel2 = new MirrorTextModel(null!, model.getLinesContent(), model.getEOL(), model.getVersionId()); let mirrorModel2PrevVersionId = model.getVersionId(); @@ -1058,8 +1058,8 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { }); let assertMirrorModels = () => { - assert.equal(mirrorModel2.getText(), model.getValue(), 'mirror model 2 text OK'); - assert.equal(mirrorModel2.version, model.getVersionId(), 'mirror model 2 version OK'); + assert.strictEqual(mirrorModel2.getText(), model.getValue(), 'mirror model 2 text OK'); + assert.strictEqual(mirrorModel2.version, model.getVersionId(), 'mirror model 2 version OK'); }; model.setEOL(EndOfLineSequence.CRLF); @@ -1077,16 +1077,16 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { { range: new Range(1, 2, 1, 2), text: '"' }, ]); - assert.equal(model.getValue(EndOfLinePreference.LF), '"\'"👁\''); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '"\'"👁\''); - assert.deepEqual(model.validateRange(new Range(1, 3, 1, 4)), new Range(1, 3, 1, 4)); + assert.deepStrictEqual(model.validateRange(new Range(1, 3, 1, 4)), new Range(1, 3, 1, 4)); model.applyEdits([ { range: new Range(1, 1, 1, 2), text: null }, { range: new Range(1, 3, 1, 4), text: null }, ]); - assert.equal(model.getValue(EndOfLinePreference.LF), '\'👁\''); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\'👁\''); model.dispose(); }); @@ -1108,7 +1108,7 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { model.applyEdits(undoEdits); - assert.deepEqual(model.getValue(), 'line1\nline2\nline3\n'); + assert.deepStrictEqual(model.getValue(), 'line1\nline2\nline3\n'); model.dispose(); }); diff --git a/src/vs/editor/test/common/model/intervalTree.test.ts b/src/vs/editor/test/common/model/intervalTree.test.ts index be89c016..58e534c0 100644 --- a/src/vs/editor/test/common/model/intervalTree.test.ts +++ b/src/vs/editor/test/common/model/intervalTree.test.ts @@ -111,7 +111,7 @@ suite('IntervalTree', () => { let actualNodes = this._tree.intervalSearch(op.begin, op.end, 0, false, 0); let actual = actualNodes.map(n => new Interval(n.cachedAbsoluteStart, n.cachedAbsoluteEnd)); let expected = this._oracle.search(new Interval(op.begin, op.end)); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); return; } @@ -123,7 +123,7 @@ suite('IntervalTree', () => { let actual = this._tree.getAllInOrder().map(n => new Interval(n.cachedAbsoluteStart, n.cachedAbsoluteEnd)); let expected = this._oracle.intervals; - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } public getExistingNodeId(index: number): number { @@ -500,7 +500,7 @@ suite('IntervalTree', () => { function assertIntervalSearch(start: number, end: number, expected: [number, number][]): void { let actualNodes = T.intervalSearch(start, end, 0, false, 0); let actual = actualNodes.map((n) => <[number, number]>[n.cachedAbsoluteStart, n.cachedAbsoluteEnd]); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } test('cormen 1->2', () => { @@ -559,7 +559,7 @@ suite('IntervalTree', () => { let node = new IntervalNode('', nodeStart, nodeEnd); setNodeStickiness(node, nodeStickiness); nodeAcceptEdit(node, start, end, textLength, forceMoveMarkers); - assert.deepEqual([node.start, node.end], [expectedNodeStart, expectedNodeEnd], msg); + assert.deepStrictEqual([node.start, node.end], [expectedNodeStart, expectedNodeEnd], msg); } test('nodeAcceptEdit', () => { diff --git a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts index 4c9bd0d1..f385fdda 100644 --- a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts @@ -33,7 +33,7 @@ suite('PieceTreeTextBuffer._getInverseEdits', () => { function assertInverseEdits(ops: IValidatedEditOperation[], expected: Range[]): void { let actual = PieceTreeTextBuffer._getInverseEditRanges(ops); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } test('single insert', () => { @@ -282,10 +282,10 @@ suite('PieceTreeTextBuffer._toSingleEditOperation', () => { } function testToSingleEditOperation(original: string[], edits: IValidatedEditOperation[], expected: IValidatedEditOperation): void { - const textBuffer = createTextBufferFactory(original.join('\n')).create(DefaultEndOfLine.LF); + const textBuffer = createTextBufferFactory(original.join('\n')).create(DefaultEndOfLine.LF).textBuffer; const actual = textBuffer._toSingleEditOperation(edits); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } test('one edit op is unchanged', () => { diff --git a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts index dafa53c0..83b4a90d 100644 --- a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts +++ b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts @@ -10,11 +10,11 @@ import { PieceTreeTextBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/ import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; export function testTextBufferFactory(text: string, eol: string, mightContainNonBasicASCII: boolean, mightContainRTL: boolean): void { - const textBuffer = createTextBufferFactory(text).create(DefaultEndOfLine.LF); + const textBuffer = createTextBufferFactory(text).create(DefaultEndOfLine.LF).textBuffer; - assert.equal(textBuffer.mightContainNonBasicASCII(), mightContainNonBasicASCII); - assert.equal(textBuffer.mightContainRTL(), mightContainRTL); - assert.equal(textBuffer.getEOL(), eol); + assert.strictEqual(textBuffer.mightContainNonBasicASCII(), mightContainNonBasicASCII); + assert.strictEqual(textBuffer.mightContainRTL(), mightContainRTL); + assert.strictEqual(textBuffer.getEOL(), eol); } suite('ModelBuilder', () => { diff --git a/src/vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils.ts b/src/vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils.ts index 4f87bf63..069b5553 100644 --- a/src/vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils.ts +++ b/src/vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils.ts @@ -6,7 +6,7 @@ import { CharCode } from 'vs/base/common/charCode'; import { splitLines } from 'vs/base/common/strings'; import { Range } from 'vs/editor/common/core/range'; -import { DefaultEndOfLine, ITextBuffer, ITextBufferBuilder, ValidAnnotatedEditOperation } from 'vs/editor/common/model'; +import { ValidAnnotatedEditOperation } from 'vs/editor/common/model'; export function getRandomInt(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1)) + min; @@ -127,25 +127,6 @@ export function generateRandomReplaces(chunks: string[], editCnt: number, search return ops; } -export function createMockText(lineCount: number, minColumn: number, maxColumn: number) { - let fixedEOL = getRandomEOLSequence(); - let lines: string[] = []; - for (let i = 0; i < lineCount; i++) { - if (i !== 0) { - lines.push(fixedEOL); - } - lines.push(getRandomString(minColumn, maxColumn)); - } - return lines.join(''); -} - -export function createMockBuffer(str: string, bufferBuilder: ITextBufferBuilder): ITextBuffer { - bufferBuilder.acceptChunk(str); - let bufferFactory = bufferBuilder.finish(); - let buffer = bufferFactory.create(DefaultEndOfLine.LF); - return buffer; -} - export function generateRandomChunkWithLF(minLength: number, maxLength: number): string { let length = getRandomInt(minLength, maxLength); let r = ''; diff --git a/src/vs/editor/test/common/model/model.line.test.ts b/src/vs/editor/test/common/model/model.line.test.ts index 139b169b..b07f2481 100644 --- a/src/vs/editor/test/common/model/model.line.test.ts +++ b/src/vs/editor/test/common/model/model.line.test.ts @@ -39,13 +39,13 @@ function assertLineTokens(__actual: LineTokens, _expected: TestToken[]): void { type: token.getType() }; }; - assert.deepEqual(actual, expected.map(decode)); + assert.deepStrictEqual(actual, expected.map(decode)); } suite('ModelLine - getIndentLevel', () => { function assertIndentLevel(text: string, expected: number, tabSize: number = 4): void { let actual = TextModel.computeIndentLevel(text, tabSize); - assert.equal(actual, expected, text); + assert.strictEqual(actual, expected, text); } test('getIndentLevel', () => { @@ -126,7 +126,7 @@ suite('ModelLinesTokens', () => { for (let lineIndex = 0; lineIndex < expected.length; lineIndex++) { const actualLine = model.getLineContent(lineIndex + 1); const actualTokens = model.getLineTokens(lineIndex + 1); - assert.equal(actualLine, expected[lineIndex].text); + assert.strictEqual(actualLine, expected[lineIndex].text); assertLineTokens(actualTokens, expected[lineIndex].tokens); } } diff --git a/src/vs/editor/test/common/model/model.modes.test.ts b/src/vs/editor/test/common/model/model.modes.test.ts index ce313b77..1d255659 100644 --- a/src/vs/editor/test/common/model/model.modes.test.ts +++ b/src/vs/editor/test/common/model/model.modes.test.ts @@ -21,14 +21,14 @@ suite('Editor Model - Model Modes 1', () => { let calledFor: string[] = []; function checkAndClear(arr: string[]) { - assert.deepEqual(calledFor, arr); + assert.deepStrictEqual(calledFor, arr); calledFor = []; } const tokenizationSupport: modes.ITokenizationSupport = { getInitialState: () => NULL_STATE, tokenize: undefined!, - tokenize2: (line: string, state: modes.IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: modes.IState): TokenizationResult2 => { calledFor.push(line.charAt(0)); return new TokenizationResult2(new Uint32Array(0), state); } @@ -106,7 +106,7 @@ suite('Editor Model - Model Modes 1', () => { checkAndClear(['1', '2', '3', '4', '5']); thisModel.applyEdits([EditOperation.insert(new Position(1, 1), '0\n-\n+')]); - assert.equal(thisModel.getLineCount(), 7); + assert.strictEqual(thisModel.getLineCount(), 7); thisModel.forceTokenization(7); checkAndClear(['0', '-', '+']); @@ -174,14 +174,14 @@ suite('Editor Model - Model Modes 2', () => { let calledFor: string[] = []; function checkAndClear(arr: string[]): void { - assert.deepEqual(calledFor, arr); + assert.deepStrictEqual(calledFor, arr); calledFor = []; } const tokenizationSupport: modes.ITokenizationSupport = { getInitialState: () => new ModelState2(''), tokenize: undefined!, - tokenize2: (line: string, state: modes.IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: modes.IState): TokenizationResult2 => { calledFor.push(line); (state).prevLineContent = line; return new TokenizationResult2(new Uint32Array(0), state); diff --git a/src/vs/editor/test/common/model/model.test.ts b/src/vs/editor/test/common/model/model.test.ts index ed6787bc..bfdcfc94 100644 --- a/src/vs/editor/test/common/model/model.test.ts +++ b/src/vs/editor/test/common/model/model.test.ts @@ -46,50 +46,50 @@ suite('Editor Model - Model', () => { // --------- insert text test('model getValue', () => { - assert.equal(thisModel.getValue(), 'My First Line\n\t\tMy Second Line\n Third Line\n\n1'); + assert.strictEqual(thisModel.getValue(), 'My First Line\n\t\tMy Second Line\n Third Line\n\n1'); }); test('model insert empty text', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 1), '')]); - assert.equal(thisModel.getLineCount(), 5); - assert.equal(thisModel.getLineContent(1), 'My First Line'); + assert.strictEqual(thisModel.getLineCount(), 5); + assert.strictEqual(thisModel.getLineContent(1), 'My First Line'); }); test('model insert text without newline 1', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'foo ')]); - assert.equal(thisModel.getLineCount(), 5); - assert.equal(thisModel.getLineContent(1), 'foo My First Line'); + assert.strictEqual(thisModel.getLineCount(), 5); + assert.strictEqual(thisModel.getLineContent(1), 'foo My First Line'); }); test('model insert text without newline 2', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' foo')]); - assert.equal(thisModel.getLineCount(), 5); - assert.equal(thisModel.getLineContent(1), 'My foo First Line'); + assert.strictEqual(thisModel.getLineCount(), 5); + assert.strictEqual(thisModel.getLineContent(1), 'My foo First Line'); }); test('model insert text with one newline', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' new line\nNo longer')]); - assert.equal(thisModel.getLineCount(), 6); - assert.equal(thisModel.getLineContent(1), 'My new line'); - assert.equal(thisModel.getLineContent(2), 'No longer First Line'); + assert.strictEqual(thisModel.getLineCount(), 6); + assert.strictEqual(thisModel.getLineContent(1), 'My new line'); + assert.strictEqual(thisModel.getLineContent(2), 'No longer First Line'); }); test('model insert text with two newlines', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' new line\nOne more line in the middle\nNo longer')]); - assert.equal(thisModel.getLineCount(), 7); - assert.equal(thisModel.getLineContent(1), 'My new line'); - assert.equal(thisModel.getLineContent(2), 'One more line in the middle'); - assert.equal(thisModel.getLineContent(3), 'No longer First Line'); + assert.strictEqual(thisModel.getLineCount(), 7); + assert.strictEqual(thisModel.getLineContent(1), 'My new line'); + assert.strictEqual(thisModel.getLineContent(2), 'One more line in the middle'); + assert.strictEqual(thisModel.getLineContent(3), 'No longer First Line'); }); test('model insert text with many newlines', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 3), '\n\n\n\n')]); - assert.equal(thisModel.getLineCount(), 9); - assert.equal(thisModel.getLineContent(1), 'My'); - assert.equal(thisModel.getLineContent(2), ''); - assert.equal(thisModel.getLineContent(3), ''); - assert.equal(thisModel.getLineContent(4), ''); - assert.equal(thisModel.getLineContent(5), ' First Line'); + assert.strictEqual(thisModel.getLineCount(), 9); + assert.strictEqual(thisModel.getLineContent(1), 'My'); + assert.strictEqual(thisModel.getLineContent(2), ''); + assert.strictEqual(thisModel.getLineContent(3), ''); + assert.strictEqual(thisModel.getLineContent(4), ''); + assert.strictEqual(thisModel.getLineContent(5), ' First Line'); }); @@ -111,7 +111,7 @@ suite('Editor Model - Model', () => { e = _e; }); thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'foo ')]); - assert.deepEqual(e, new ModelRawContentChangedEvent( + assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 'foo My First Line') ], @@ -130,7 +130,7 @@ suite('Editor Model - Model', () => { e = _e; }); thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' new line\nNo longer')]); - assert.deepEqual(e, new ModelRawContentChangedEvent( + assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 'My new line'), new ModelRawLinesInserted(2, 2, ['No longer First Line']), @@ -146,47 +146,47 @@ suite('Editor Model - Model', () => { test('model delete empty text', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 1))]); - assert.equal(thisModel.getLineCount(), 5); - assert.equal(thisModel.getLineContent(1), 'My First Line'); + assert.strictEqual(thisModel.getLineCount(), 5); + assert.strictEqual(thisModel.getLineContent(1), 'My First Line'); }); test('model delete text from one line', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 2))]); - assert.equal(thisModel.getLineCount(), 5); - assert.equal(thisModel.getLineContent(1), 'y First Line'); + assert.strictEqual(thisModel.getLineCount(), 5); + assert.strictEqual(thisModel.getLineContent(1), 'y First Line'); }); test('model delete text from one line 2', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'a')]); - assert.equal(thisModel.getLineContent(1), 'aMy First Line'); + assert.strictEqual(thisModel.getLineContent(1), 'aMy First Line'); thisModel.applyEdits([EditOperation.delete(new Range(1, 2, 1, 4))]); - assert.equal(thisModel.getLineCount(), 5); - assert.equal(thisModel.getLineContent(1), 'a First Line'); + assert.strictEqual(thisModel.getLineCount(), 5); + assert.strictEqual(thisModel.getLineContent(1), 'a First Line'); }); test('model delete all text from a line', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 14))]); - assert.equal(thisModel.getLineCount(), 5); - assert.equal(thisModel.getLineContent(1), ''); + assert.strictEqual(thisModel.getLineCount(), 5); + assert.strictEqual(thisModel.getLineContent(1), ''); }); test('model delete text from two lines', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 2, 6))]); - assert.equal(thisModel.getLineCount(), 4); - assert.equal(thisModel.getLineContent(1), 'My Second Line'); + assert.strictEqual(thisModel.getLineCount(), 4); + assert.strictEqual(thisModel.getLineContent(1), 'My Second Line'); }); test('model delete text from many lines', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 3, 5))]); - assert.equal(thisModel.getLineCount(), 3); - assert.equal(thisModel.getLineContent(1), 'My Third Line'); + assert.strictEqual(thisModel.getLineCount(), 3); + assert.strictEqual(thisModel.getLineContent(1), 'My Third Line'); }); test('model delete everything', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 5, 2))]); - assert.equal(thisModel.getLineCount(), 1); - assert.equal(thisModel.getLineContent(1), ''); + assert.strictEqual(thisModel.getLineCount(), 1); + assert.strictEqual(thisModel.getLineContent(1), ''); }); // --------- delete text eventing @@ -207,7 +207,7 @@ suite('Editor Model - Model', () => { e = _e; }); thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 2))]); - assert.deepEqual(e, new ModelRawContentChangedEvent( + assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 'y First Line'), ], @@ -226,7 +226,7 @@ suite('Editor Model - Model', () => { e = _e; }); thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 14))]); - assert.deepEqual(e, new ModelRawContentChangedEvent( + assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, ''), ], @@ -245,7 +245,7 @@ suite('Editor Model - Model', () => { e = _e; }); thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 2, 6))]); - assert.deepEqual(e, new ModelRawContentChangedEvent( + assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 'My Second Line'), new ModelRawLinesDeleted(2, 2), @@ -265,7 +265,7 @@ suite('Editor Model - Model', () => { e = _e; }); thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 3, 5))]); - assert.deepEqual(e, new ModelRawContentChangedEvent( + assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 'My Third Line'), new ModelRawLinesDeleted(2, 3), @@ -279,31 +279,31 @@ suite('Editor Model - Model', () => { // --------- getValueInRange test('getValueInRange', () => { - assert.equal(thisModel.getValueInRange(new Range(1, 1, 1, 1)), ''); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 1, 2)), 'M'); - assert.equal(thisModel.getValueInRange(new Range(1, 2, 1, 3)), 'y'); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 1, 14)), 'My First Line'); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 2, 1)), 'My First Line\n'); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 2, 2)), 'My First Line\n\t'); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 2, 3)), 'My First Line\n\t\t'); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 2, 17)), 'My First Line\n\t\tMy Second Line'); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 3, 1)), 'My First Line\n\t\tMy Second Line\n'); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 4, 1)), 'My First Line\n\t\tMy Second Line\n Third Line\n'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 1, 1)), ''); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 1, 2)), 'M'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 2, 1, 3)), 'y'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 1, 14)), 'My First Line'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 1)), 'My First Line\n'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 2)), 'My First Line\n\t'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 3)), 'My First Line\n\t\t'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 17)), 'My First Line\n\t\tMy Second Line'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 3, 1)), 'My First Line\n\t\tMy Second Line\n'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 4, 1)), 'My First Line\n\t\tMy Second Line\n Third Line\n'); }); // --------- getValueLengthInRange test('getValueLengthInRange', () => { - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 1, 14)), 'My First Line'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 2, 1)), 'My First Line\n'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 2, 2)), 'My First Line\n\t'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 2, 3)), 'My First Line\n\t\t'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 2, 17)), 'My First Line\n\t\tMy Second Line'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 3, 1)), 'My First Line\n\t\tMy Second Line\n'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 4, 1)), 'My First Line\n\t\tMy Second Line\n Third Line\n'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 1, 14)), 'My First Line'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 1)), 'My First Line\n'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 2)), 'My First Line\n\t'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 3)), 'My First Line\n\t\t'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 17)), 'My First Line\n\t\tMy Second Line'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 3, 1)), 'My First Line\n\t\tMy Second Line\n'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 4, 1)), 'My First Line\n\t\tMy Second Line\n Third Line\n'.length); }); // --------- setValue @@ -316,7 +316,7 @@ suite('Editor Model - Model', () => { e = _e; }); thisModel.setValue('new value'); - assert.deepEqual(e, new ModelRawContentChangedEvent( + assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawFlush() ], @@ -332,8 +332,8 @@ suite('Editor Model - Model', () => { { range: new Range(1, 1, 1, 1), text: 'b' }, ], true); - assert.deepEqual(res[0].range, new Range(2, 1, 2, 2)); - assert.deepEqual(res[1].range, new Range(1, 1, 1, 2)); + assert.deepStrictEqual(res[0].range, new Range(2, 1, 2, 2)); + assert.deepStrictEqual(res[1].range, new Range(1, 1, 1, 2)); }); }); @@ -358,17 +358,17 @@ suite('Editor Model - Model Line Separators', () => { }); test('model getValue', () => { - assert.equal(thisModel.getValue(), 'My First Line\u2028\t\tMy Second Line\n Third Line\u2028\n1'); + assert.strictEqual(thisModel.getValue(), 'My First Line\u2028\t\tMy Second Line\n Third Line\u2028\n1'); }); test('model lines', () => { - assert.equal(thisModel.getLineCount(), 3); + assert.strictEqual(thisModel.getLineCount(), 3); }); test('Bug 13333:Model should line break on lonely CR too', () => { let model = createTextModel('Hello\rWorld!\r\nAnother line'); - assert.equal(model.getLineCount(), 3); - assert.equal(model.getValue(), 'Hello\r\nWorld!\r\nAnother line'); + assert.strictEqual(model.getLineCount(), 3); + assert.strictEqual(model.getValue(), 'Hello\r\nWorld!\r\nAnother line'); model.dispose(); }); }); @@ -389,7 +389,7 @@ suite('Editor Model - Words', () => { this._register(TokenizationRegistry.register(this.getLanguageIdentifier().language, { getInitialState: (): IState => NULL_STATE, tokenize: undefined!, - tokenize2: (line: string, state: IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: IState): TokenizationResult2 => { const tokensArr: number[] = []; let prevLanguageId: LanguageIdentifier | undefined = undefined; for (let i = 0; i < line.length; i++) { @@ -434,17 +434,17 @@ suite('Editor Model - Words', () => { const thisModel = createTextModel(text.join('\n')); disposables.push(thisModel); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: 'This', startColumn: 1, endColumn: 5 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 2)), { word: 'This', startColumn: 1, endColumn: 5 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 4)), { word: 'This', startColumn: 1, endColumn: 5 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 5)), { word: 'This', startColumn: 1, endColumn: 5 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 6)), { word: 'text', startColumn: 6, endColumn: 10 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 19)), { word: 'some', startColumn: 15, endColumn: 19 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 20)), null); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 21)), { word: 'words', startColumn: 21, endColumn: 26 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 26)), { word: 'words', startColumn: 21, endColumn: 26 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 27)), null); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 28)), null); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: 'This', startColumn: 1, endColumn: 5 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 2)), { word: 'This', startColumn: 1, endColumn: 5 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 4)), { word: 'This', startColumn: 1, endColumn: 5 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 5)), { word: 'This', startColumn: 1, endColumn: 5 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 6)), { word: 'text', startColumn: 6, endColumn: 10 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 19)), { word: 'some', startColumn: 15, endColumn: 19 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 20)), null); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 21)), { word: 'words', startColumn: 21, endColumn: 26 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 26)), { word: 'words', startColumn: 21, endColumn: 26 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 27)), null); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 28)), null); }); test('getWordAtPosition at embedded language boundaries', () => { @@ -455,13 +455,13 @@ suite('Editor Model - Words', () => { const model = createTextModel('abab', undefined, outerMode.getLanguageIdentifier()); disposables.push(model); - assert.deepEqual(model.getWordAtPosition(new Position(1, 1)), { word: 'ab', startColumn: 1, endColumn: 3 }); - assert.deepEqual(model.getWordAtPosition(new Position(1, 2)), { word: 'ab', startColumn: 1, endColumn: 3 }); - assert.deepEqual(model.getWordAtPosition(new Position(1, 3)), { word: 'ab', startColumn: 1, endColumn: 3 }); - assert.deepEqual(model.getWordAtPosition(new Position(1, 4)), { word: 'xx', startColumn: 4, endColumn: 6 }); - assert.deepEqual(model.getWordAtPosition(new Position(1, 5)), { word: 'xx', startColumn: 4, endColumn: 6 }); - assert.deepEqual(model.getWordAtPosition(new Position(1, 6)), { word: 'xx', startColumn: 4, endColumn: 6 }); - assert.deepEqual(model.getWordAtPosition(new Position(1, 7)), { word: 'ab', startColumn: 7, endColumn: 9 }); + assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 1)), { word: 'ab', startColumn: 1, endColumn: 3 }); + assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 2)), { word: 'ab', startColumn: 1, endColumn: 3 }); + assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 3)), { word: 'ab', startColumn: 1, endColumn: 3 }); + assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 4)), { word: 'xx', startColumn: 4, endColumn: 6 }); + assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 5)), { word: 'xx', startColumn: 4, endColumn: 6 }); + assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 6)), { word: 'xx', startColumn: 4, endColumn: 6 }); + assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 7)), { word: 'ab', startColumn: 7, endColumn: 9 }); }); test('issue #61296: VS code freezes when editing CSS file with emoji', () => { @@ -480,13 +480,13 @@ suite('Editor Model - Words', () => { const thisModel = createTextModel('.🐷-a-b', undefined, MODE_ID); disposables.push(thisModel); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: '.', startColumn: 1, endColumn: 2 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 2)), { word: '.', startColumn: 1, endColumn: 2 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 3)), null); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 4)), { word: '-a-b', startColumn: 4, endColumn: 8 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 5)), { word: '-a-b', startColumn: 4, endColumn: 8 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 6)), { word: '-a-b', startColumn: 4, endColumn: 8 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 7)), { word: '-a-b', startColumn: 4, endColumn: 8 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 8)), { word: '-a-b', startColumn: 4, endColumn: 8 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: '.', startColumn: 1, endColumn: 2 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 2)), { word: '.', startColumn: 1, endColumn: 2 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 3)), null); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 4)), { word: '-a-b', startColumn: 4, endColumn: 8 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 5)), { word: '-a-b', startColumn: 4, endColumn: 8 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 6)), { word: '-a-b', startColumn: 4, endColumn: 8 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 7)), { word: '-a-b', startColumn: 4, endColumn: 8 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 8)), { word: '-a-b', startColumn: 4, endColumn: 8 }); }); }); diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index 4f7690f1..da7b0093 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -28,7 +28,7 @@ function modelHasDecorations(model: TextModel, decorations: ILightWeightDecorati }); } modelDecorations.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); - assert.deepEqual(modelDecorations, decorations); + assert.deepStrictEqual(modelDecorations, decorations); } function modelHasDecoration(model: TextModel, startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, className: string) { @@ -39,7 +39,7 @@ function modelHasDecoration(model: TextModel, startLineNumber: number, startColu } function modelHasNoDecorations(model: TextModel) { - assert.equal(model.getAllDecorations().length, 0, 'Model has no decoration'); + assert.strictEqual(model.getAllDecorations().length, 0, 'Model has no decoration'); } function addDecoration(model: TextModel, startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, className: string): string { @@ -60,7 +60,7 @@ function lineHasDecorations(model: TextModel, lineNumber: number, decorations: { className: decs[i].options.className }); } - assert.deepEqual(lineDecorations, decorations, 'Line decorations'); + assert.deepStrictEqual(lineDecorations, decorations, 'Line decorations'); } function lineHasNoDecorations(model: TextModel, lineNumber: number) { @@ -122,12 +122,12 @@ suite('Editor Model - Model Decorations', () => { addDecoration(thisModel, 1, 1, 2, 1, 'myType'); let line1Decorations = thisModel.getLineDecorations(1); - assert.equal(line1Decorations.length, 1); - assert.equal(line1Decorations[0].options.className, 'myType'); + assert.strictEqual(line1Decorations.length, 1); + assert.strictEqual(line1Decorations[0].options.className, 'myType'); let line2Decorations = thisModel.getLineDecorations(1); - assert.equal(line2Decorations.length, 1); - assert.equal(line2Decorations[0].options.className, 'myType'); + assert.strictEqual(line2Decorations.length, 1); + assert.strictEqual(line2Decorations[0].options.className, 'myType'); lineHasNoDecorations(thisModel, 3); lineHasNoDecorations(thisModel, 4); @@ -138,16 +138,16 @@ suite('Editor Model - Model Decorations', () => { addDecoration(thisModel, 1, 2, 3, 2, 'myType'); let line1Decorations = thisModel.getLineDecorations(1); - assert.equal(line1Decorations.length, 1); - assert.equal(line1Decorations[0].options.className, 'myType'); + assert.strictEqual(line1Decorations.length, 1); + assert.strictEqual(line1Decorations[0].options.className, 'myType'); let line2Decorations = thisModel.getLineDecorations(1); - assert.equal(line2Decorations.length, 1); - assert.equal(line2Decorations[0].options.className, 'myType'); + assert.strictEqual(line2Decorations.length, 1); + assert.strictEqual(line2Decorations[0].options.className, 'myType'); let line3Decorations = thisModel.getLineDecorations(1); - assert.equal(line3Decorations.length, 1); - assert.equal(line3Decorations[0].options.className, 'myType'); + assert.strictEqual(line3Decorations.length, 1); + assert.strictEqual(line3Decorations[0].options.className, 'myType'); lineHasNoDecorations(thisModel, 4); lineHasNoDecorations(thisModel, 5); @@ -209,7 +209,7 @@ suite('Editor Model - Model Decorations', () => { listenerCalled++; }); addDecoration(thisModel, 1, 2, 3, 2, 'myType'); - assert.equal(listenerCalled, 1, 'listener called'); + assert.strictEqual(listenerCalled, 1, 'listener called'); }); test('decorations emit event on change', () => { @@ -221,7 +221,7 @@ suite('Editor Model - Model Decorations', () => { thisModel.changeDecorations((changeAccessor) => { changeAccessor.changeDecoration(decId, new Range(1, 1, 1, 2)); }); - assert.equal(listenerCalled, 1, 'listener called'); + assert.strictEqual(listenerCalled, 1, 'listener called'); }); test('decorations emit event on remove', () => { @@ -233,7 +233,7 @@ suite('Editor Model - Model Decorations', () => { thisModel.changeDecorations((changeAccessor) => { changeAccessor.removeDecoration(decId); }); - assert.equal(listenerCalled, 1, 'listener called'); + assert.strictEqual(listenerCalled, 1, 'listener called'); }); test('decorations emit event when inserting one line text before it', () => { @@ -245,7 +245,7 @@ suite('Editor Model - Model Decorations', () => { }); thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'Hallo ')]); - assert.equal(listenerCalled, 1, 'listener called'); + assert.strictEqual(listenerCalled, 1, 'listener called'); }); test('decorations do not emit event on no-op deltaDecorations', () => { @@ -260,7 +260,7 @@ suite('Editor Model - Model Decorations', () => { accessor.deltaDecorations([], []); }); - assert.equal(listenerCalled, 0, 'listener not called'); + assert.strictEqual(listenerCalled, 0, 'listener not called'); }); // --------- editing text & effects on decorations @@ -429,7 +429,7 @@ suite('Decorations and editing', () => { forceMoveMarkers: editForceMoveMarkers }]); const actual = model.getDecorationRange(id); - assert.deepEqual(actual, expectedDecRange, msg); + assert.deepStrictEqual(actual, expectedDecRange, msg); model.dispose(); } @@ -1155,20 +1155,20 @@ suite('deltaDecorations', () => { let initialIds = model.deltaDecorations([], decorations.map(toModelDeltaDecoration)); let actualDecorations = readModelDecorations(model, initialIds); - assert.equal(initialIds.length, decorations.length, 'returns expected cnt of ids'); - assert.equal(initialIds.length, model.getAllDecorations().length, 'does not leak decorations'); + assert.strictEqual(initialIds.length, decorations.length, 'returns expected cnt of ids'); + assert.strictEqual(initialIds.length, model.getAllDecorations().length, 'does not leak decorations'); actualDecorations.sort((a, b) => strcmp(a.id, b.id)); decorations.sort((a, b) => strcmp(a.id, b.id)); - assert.deepEqual(actualDecorations, decorations); + assert.deepStrictEqual(actualDecorations, decorations); let newIds = model.deltaDecorations(initialIds, newDecorations.map(toModelDeltaDecoration)); let actualNewDecorations = readModelDecorations(model, newIds); - assert.equal(newIds.length, newDecorations.length, 'returns expected cnt of ids'); - assert.equal(newIds.length, model.getAllDecorations().length, 'does not leak decorations'); + assert.strictEqual(newIds.length, newDecorations.length, 'returns expected cnt of ids'); + assert.strictEqual(newIds.length, model.getAllDecorations().length, 'does not leak decorations'); actualNewDecorations.sort((a, b) => strcmp(a.id, b.id)); newDecorations.sort((a, b) => strcmp(a.id, b.id)); - assert.deepEqual(actualDecorations, decorations); + assert.deepStrictEqual(actualDecorations, decorations); model.dispose(); } @@ -1188,8 +1188,8 @@ suite('deltaDecorations', () => { toModelDeltaDecoration(decoration('b', 2, 1, 2, 13)) ]); - assert.deepEqual(model.getDecorationRange(ids[0]), range(1, 1, 1, 12)); - assert.deepEqual(model.getDecorationRange(ids[1]), range(2, 1, 2, 13)); + assert.deepStrictEqual(model.getDecorationRange(ids[0]), range(1, 1, 1, 12)); + assert.deepStrictEqual(model.getDecorationRange(ids[1]), range(2, 1, 2, 13)); model.dispose(); }); @@ -1294,7 +1294,7 @@ suite('deltaDecorations', () => { let actualDecoration = model.getDecorationOptions(ids[0]); - assert.deepEqual(actualDecoration!.hoverMessage, { value: 'hello2' }); + assert.deepStrictEqual(actualDecoration!.hoverMessage, { value: 'hello2' }); model.dispose(); }); @@ -1326,16 +1326,16 @@ suite('deltaDecorations', () => { toModelDeltaDecoration(decoration('b', 2, 1, 2, 13)) ]); - assert.deepEqual(model.getDecorationRange(ids[0]), range(1, 1, 1, 12)); - assert.deepEqual(model.getDecorationRange(ids[1]), range(2, 1, 2, 13)); + assert.deepStrictEqual(model.getDecorationRange(ids[0]), range(1, 1, 1, 12)); + assert.deepStrictEqual(model.getDecorationRange(ids[1]), range(2, 1, 2, 13)); ids = model.deltaDecorations(ids, [ toModelDeltaDecoration(decoration('a', 1, 1, 1, 12)), toModelDeltaDecoration(decoration('b', 2, 1, 2, 13)) ]); - assert.deepEqual(model.getDecorationRange(ids[0]), range(1, 1, 1, 12)); - assert.deepEqual(model.getDecorationRange(ids[1]), range(2, 1, 2, 13)); + assert.deepStrictEqual(model.getDecorationRange(ids[0]), range(1, 1, 1, 12)); + assert.deepStrictEqual(model.getDecorationRange(ids[1]), range(2, 1, 2, 13)); model.dispose(); }); @@ -1365,7 +1365,7 @@ suite('deltaDecorations', () => { let inRangeClassNames = inRange.map(d => d.options.className); inRangeClassNames.sort(); - assert.deepEqual(inRangeClassNames, ['x1', 'x2', 'x3', 'x4']); + assert.deepStrictEqual(inRangeClassNames, ['x1', 'x2', 'x3', 'x4']); model.dispose(); }); @@ -1383,7 +1383,7 @@ suite('deltaDecorations', () => { forceMoveMarkers: false }]); const actual = model.getDecorationRange(id); - assert.deepEqual(actual, new Range(1, 1, 1, 1)); + assert.deepStrictEqual(actual, new Range(1, 1, 1, 1)); model.dispose(); }); diff --git a/src/vs/editor/test/common/model/modelEditOperation.test.ts b/src/vs/editor/test/common/model/modelEditOperation.test.ts index 058c5d3d..d6c2af9b 100644 --- a/src/vs/editor/test/common/model/modelEditOperation.test.ts +++ b/src/vs/editor/test/common/model/modelEditOperation.test.ts @@ -52,19 +52,19 @@ suite('Editor Model - Model Edit Operation', () => { let inverseEditOp = model.applyEdits(editOp, true); - assert.equal(model.getLineCount(), editedLines.length); + assert.strictEqual(model.getLineCount(), editedLines.length); for (let i = 0; i < editedLines.length; i++) { - assert.equal(model.getLineContent(i + 1), editedLines[i]); + assert.strictEqual(model.getLineContent(i + 1), editedLines[i]); } let originalOp = model.applyEdits(inverseEditOp, true); - assert.equal(model.getLineCount(), 5); - assert.equal(model.getLineContent(1), LINE1); - assert.equal(model.getLineContent(2), LINE2); - assert.equal(model.getLineContent(3), LINE3); - assert.equal(model.getLineContent(4), LINE4); - assert.equal(model.getLineContent(5), LINE5); + assert.strictEqual(model.getLineCount(), 5); + assert.strictEqual(model.getLineContent(1), LINE1); + assert.strictEqual(model.getLineContent(2), LINE2); + assert.strictEqual(model.getLineContent(3), LINE3); + assert.strictEqual(model.getLineContent(4), LINE4); + assert.strictEqual(model.getLineContent(5), LINE5); const simplifyEdit = (edit: IIdentifiedSingleEditOperation) => { return { @@ -75,7 +75,7 @@ suite('Editor Model - Model Edit Operation', () => { isAutoWhitespaceEdit: edit.isAutoWhitespaceEdit || false }; }; - assert.deepEqual(originalOp.map(simplifyEdit), editOp.map(simplifyEdit)); + assert.deepStrictEqual(originalOp.map(simplifyEdit), editOp.map(simplifyEdit)); } test('Insert inline', () => { diff --git a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts index 019b23f4..04fc6d74 100644 --- a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts @@ -77,11 +77,11 @@ function trimLineFeed(text: string): string { function testLinesContent(str: string, pieceTable: PieceTreeBase) { let lines = splitLines(str); - assert.equal(pieceTable.getLineCount(), lines.length); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLineCount(), lines.length); + assert.strictEqual(pieceTable.getLinesRawContent(), str); for (let i = 0; i < lines.length; i++) { - assert.equal(pieceTable.getLineContent(i + 1), lines[i]); - assert.equal( + assert.strictEqual(pieceTable.getLineContent(i + 1), lines[i]); + assert.strictEqual( trimLineFeed( pieceTable.getValueInRange( new Range( @@ -136,16 +136,16 @@ function testLineStarts(str: string, pieceTable: PieceTreeBase) { } while (m); for (let i = 0; i < lineStarts.length; i++) { - assert.deepEqual( + assert.deepStrictEqual( pieceTable.getPositionAt(lineStarts[i]), new Position(i + 1, 1) ); - assert.equal(pieceTable.getOffsetAt(i + 1, 1), lineStarts[i]); + assert.strictEqual(pieceTable.getOffsetAt(i + 1, 1), lineStarts[i]); } for (let i = 1; i < lineStarts.length; i++) { let pos = pieceTable.getPositionAt(lineStarts[i] - 1); - assert.equal( + assert.strictEqual( pieceTable.getOffsetAt(pos.lineNumber, pos.column), lineStarts[i] - 1 ); @@ -158,7 +158,7 @@ function createTextBuffer(val: string[], normalizeEOL: boolean = true): PieceTre bufferBuilder.acceptChunk(chunk); } let factory = bufferBuilder.finish(normalizeEOL); - return (factory.create(DefaultEndOfLine.LF)).getPieceTree(); + return (factory.create(DefaultEndOfLine.LF).textBuffer).getPieceTree(); } function assertTreeInvariants(T: PieceTreeBase): void { @@ -219,12 +219,12 @@ suite('inserts and deletes', () => { ]); pieceTable.insert(34, 'This is some more text to insert at offset 34.'); - assert.equal( + assert.strictEqual( pieceTable.getLinesRawContent(), 'This is a document with some text.This is some more text to insert at offset 34.' ); pieceTable.delete(42, 5); - assert.equal( + assert.strictEqual( pieceTable.getLinesRawContent(), 'This is a document with some text.This is more text to insert at offset 34.' ); @@ -235,28 +235,28 @@ suite('inserts and deletes', () => { let pt = createTextBuffer(['']); pt.insert(0, 'AAA'); - assert.equal(pt.getLinesRawContent(), 'AAA'); + assert.strictEqual(pt.getLinesRawContent(), 'AAA'); pt.insert(0, 'BBB'); - assert.equal(pt.getLinesRawContent(), 'BBBAAA'); + assert.strictEqual(pt.getLinesRawContent(), 'BBBAAA'); pt.insert(6, 'CCC'); - assert.equal(pt.getLinesRawContent(), 'BBBAAACCC'); + assert.strictEqual(pt.getLinesRawContent(), 'BBBAAACCC'); pt.insert(5, 'DDD'); - assert.equal(pt.getLinesRawContent(), 'BBBAADDDACCC'); + assert.strictEqual(pt.getLinesRawContent(), 'BBBAADDDACCC'); assertTreeInvariants(pt); }); test('more deletes', () => { let pt = createTextBuffer(['012345678']); pt.delete(8, 1); - assert.equal(pt.getLinesRawContent(), '01234567'); + assert.strictEqual(pt.getLinesRawContent(), '01234567'); pt.delete(0, 1); - assert.equal(pt.getLinesRawContent(), '1234567'); + assert.strictEqual(pt.getLinesRawContent(), '1234567'); pt.delete(5, 1); - assert.equal(pt.getLinesRawContent(), '123457'); + assert.strictEqual(pt.getLinesRawContent(), '123457'); pt.delete(5, 1); - assert.equal(pt.getLinesRawContent(), '12345'); + assert.strictEqual(pt.getLinesRawContent(), '12345'); pt.delete(0, 5); - assert.equal(pt.getLinesRawContent(), ''); + assert.strictEqual(pt.getLinesRawContent(), ''); assertTreeInvariants(pt); }); @@ -265,17 +265,17 @@ suite('inserts and deletes', () => { let pieceTable = createTextBuffer(['']); pieceTable.insert(0, 'ceLPHmFzvCtFeHkCBej '); str = str.substring(0, 0) + 'ceLPHmFzvCtFeHkCBej ' + str.substring(0); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.insert(8, 'gDCEfNYiBUNkSwtvB K '); str = str.substring(0, 8) + 'gDCEfNYiBUNkSwtvB K ' + str.substring(8); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.insert(38, 'cyNcHxjNPPoehBJldLS '); str = str.substring(0, 38) + 'cyNcHxjNPPoehBJldLS ' + str.substring(38); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.insert(59, 'ejMx\nOTgWlbpeDExjOk '); str = str.substring(0, 59) + 'ejMx\nOTgWlbpeDExjOk ' + str.substring(59); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -293,7 +293,7 @@ suite('inserts and deletes', () => { pieceTable.insert(10, 'Gbtp '); str = str.substring(0, 10) + 'Gbtp ' + str.substring(10); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -310,7 +310,7 @@ suite('inserts and deletes', () => { str = str.substring(0, 2) + 'GGZB' + str.substring(2); pieceTable.insert(12, 'wXpq'); str = str.substring(0, 12) + 'wXpq' + str.substring(12); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); }); test('random delete 1', () => { @@ -319,30 +319,30 @@ suite('inserts and deletes', () => { pieceTable.insert(0, 'vfb'); str = str.substring(0, 0) + 'vfb' + str.substring(0); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.insert(0, 'zRq'); str = str.substring(0, 0) + 'zRq' + str.substring(0); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.delete(5, 1); str = str.substring(0, 5) + str.substring(5 + 1); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.insert(1, 'UNw'); str = str.substring(0, 1) + 'UNw' + str.substring(1); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.delete(4, 3); str = str.substring(0, 4) + str.substring(4 + 3); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.delete(1, 4); str = str.substring(0, 1) + str.substring(1 + 4); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.delete(0, 1); str = str.substring(0, 0) + str.substring(0 + 1); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -368,7 +368,7 @@ suite('inserts and deletes', () => { str = str.substring(0, 6) + str.substring(6 + 7); pieceTable.delete(3, 5); str = str.substring(0, 3) + str.substring(3 + 5); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -401,7 +401,7 @@ suite('inserts and deletes', () => { str = str.substring(0, 5) + str.substring(5 + 8); pieceTable.delete(3, 4); str = str.substring(0, 3) + str.substring(3 + 4); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -427,7 +427,7 @@ suite('inserts and deletes', () => { pieceTable.insert(5, '\n\na\r'); str = str.substring(0, 5) + '\n\na\r' + str.substring(5); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -455,7 +455,7 @@ suite('inserts and deletes', () => { pieceTable.insert(2, 'a\ra\n'); str = str.substring(0, 2) + 'a\ra\n' + str.substring(2); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -480,11 +480,11 @@ suite('inserts and deletes', () => { str = str.substring(0, 5) + '\n\na\r' + str.substring(5); pieceTable.insert(10, '\r\r\n\r'); str = str.substring(0, 10) + '\r\r\n\r' + str.substring(10); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.delete(21, 3); str = str.substring(0, 21) + str.substring(21 + 3); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -512,7 +512,7 @@ suite('inserts and deletes', () => { pieceTable.insert(3, 'a\naa'); str = str.substring(0, 3) + 'a\naa' + str.substring(3); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); test('random insert/delete \\r bug 5', () => { @@ -539,7 +539,7 @@ suite('inserts and deletes', () => { pieceTable.insert(15, '\n\r\r\r'); str = str.substring(0, 15) + '\n\r\r\r' + str.substring(15); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); }); @@ -548,22 +548,22 @@ suite('prefix sum for line feed', () => { test('basic', () => { let pieceTable = createTextBuffer(['1\n2\n3\n4']); - assert.equal(pieceTable.getLineCount(), 4); - assert.deepEqual(pieceTable.getPositionAt(0), new Position(1, 1)); - assert.deepEqual(pieceTable.getPositionAt(1), new Position(1, 2)); - assert.deepEqual(pieceTable.getPositionAt(2), new Position(2, 1)); - assert.deepEqual(pieceTable.getPositionAt(3), new Position(2, 2)); - assert.deepEqual(pieceTable.getPositionAt(4), new Position(3, 1)); - assert.deepEqual(pieceTable.getPositionAt(5), new Position(3, 2)); - assert.deepEqual(pieceTable.getPositionAt(6), new Position(4, 1)); + assert.strictEqual(pieceTable.getLineCount(), 4); + assert.deepStrictEqual(pieceTable.getPositionAt(0), new Position(1, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(1), new Position(1, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(2), new Position(2, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(3), new Position(2, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(4), new Position(3, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(5), new Position(3, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(6), new Position(4, 1)); - assert.equal(pieceTable.getOffsetAt(1, 1), 0); - assert.equal(pieceTable.getOffsetAt(1, 2), 1); - assert.equal(pieceTable.getOffsetAt(2, 1), 2); - assert.equal(pieceTable.getOffsetAt(2, 2), 3); - assert.equal(pieceTable.getOffsetAt(3, 1), 4); - assert.equal(pieceTable.getOffsetAt(3, 2), 5); - assert.equal(pieceTable.getOffsetAt(4, 1), 6); + assert.strictEqual(pieceTable.getOffsetAt(1, 1), 0); + assert.strictEqual(pieceTable.getOffsetAt(1, 2), 1); + assert.strictEqual(pieceTable.getOffsetAt(2, 1), 2); + assert.strictEqual(pieceTable.getOffsetAt(2, 2), 3); + assert.strictEqual(pieceTable.getOffsetAt(3, 1), 4); + assert.strictEqual(pieceTable.getOffsetAt(3, 2), 5); + assert.strictEqual(pieceTable.getOffsetAt(4, 1), 6); assertTreeInvariants(pieceTable); }); @@ -571,9 +571,9 @@ suite('prefix sum for line feed', () => { let pieceTable = createTextBuffer(['a\nb\nc\nde']); pieceTable.insert(8, 'fh\ni\njk'); - assert.equal(pieceTable.getLineCount(), 6); - assert.deepEqual(pieceTable.getPositionAt(9), new Position(4, 4)); - assert.equal(pieceTable.getOffsetAt(1, 1), 0); + assert.strictEqual(pieceTable.getLineCount(), 6); + assert.deepStrictEqual(pieceTable.getPositionAt(9), new Position(4, 4)); + assert.strictEqual(pieceTable.getOffsetAt(1, 1), 0); assertTreeInvariants(pieceTable); }); @@ -581,22 +581,22 @@ suite('prefix sum for line feed', () => { let pieceTable = createTextBuffer(['a\nb\nc\nde']); pieceTable.insert(7, 'fh\ni\njk'); - assert.equal(pieceTable.getLineCount(), 6); - assert.deepEqual(pieceTable.getPositionAt(6), new Position(4, 1)); - assert.deepEqual(pieceTable.getPositionAt(7), new Position(4, 2)); - assert.deepEqual(pieceTable.getPositionAt(8), new Position(4, 3)); - assert.deepEqual(pieceTable.getPositionAt(9), new Position(4, 4)); - assert.deepEqual(pieceTable.getPositionAt(12), new Position(6, 1)); - assert.deepEqual(pieceTable.getPositionAt(13), new Position(6, 2)); - assert.deepEqual(pieceTable.getPositionAt(14), new Position(6, 3)); + assert.strictEqual(pieceTable.getLineCount(), 6); + assert.deepStrictEqual(pieceTable.getPositionAt(6), new Position(4, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(7), new Position(4, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(8), new Position(4, 3)); + assert.deepStrictEqual(pieceTable.getPositionAt(9), new Position(4, 4)); + assert.deepStrictEqual(pieceTable.getPositionAt(12), new Position(6, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(13), new Position(6, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(14), new Position(6, 3)); - assert.equal(pieceTable.getOffsetAt(4, 1), 6); - assert.equal(pieceTable.getOffsetAt(4, 2), 7); - assert.equal(pieceTable.getOffsetAt(4, 3), 8); - assert.equal(pieceTable.getOffsetAt(4, 4), 9); - assert.equal(pieceTable.getOffsetAt(6, 1), 12); - assert.equal(pieceTable.getOffsetAt(6, 2), 13); - assert.equal(pieceTable.getOffsetAt(6, 3), 14); + assert.strictEqual(pieceTable.getOffsetAt(4, 1), 6); + assert.strictEqual(pieceTable.getOffsetAt(4, 2), 7); + assert.strictEqual(pieceTable.getOffsetAt(4, 3), 8); + assert.strictEqual(pieceTable.getOffsetAt(4, 4), 9); + assert.strictEqual(pieceTable.getOffsetAt(6, 1), 12); + assert.strictEqual(pieceTable.getOffsetAt(6, 2), 13); + assert.strictEqual(pieceTable.getOffsetAt(6, 3), 14); assertTreeInvariants(pieceTable); }); @@ -604,23 +604,23 @@ suite('prefix sum for line feed', () => { let pieceTable = createTextBuffer(['a\nb\nc\ndefh\ni\njk']); pieceTable.delete(7, 2); - assert.equal(pieceTable.getLinesRawContent(), 'a\nb\nc\ndh\ni\njk'); - assert.equal(pieceTable.getLineCount(), 6); - assert.deepEqual(pieceTable.getPositionAt(6), new Position(4, 1)); - assert.deepEqual(pieceTable.getPositionAt(7), new Position(4, 2)); - assert.deepEqual(pieceTable.getPositionAt(8), new Position(4, 3)); - assert.deepEqual(pieceTable.getPositionAt(9), new Position(5, 1)); - assert.deepEqual(pieceTable.getPositionAt(11), new Position(6, 1)); - assert.deepEqual(pieceTable.getPositionAt(12), new Position(6, 2)); - assert.deepEqual(pieceTable.getPositionAt(13), new Position(6, 3)); + assert.strictEqual(pieceTable.getLinesRawContent(), 'a\nb\nc\ndh\ni\njk'); + assert.strictEqual(pieceTable.getLineCount(), 6); + assert.deepStrictEqual(pieceTable.getPositionAt(6), new Position(4, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(7), new Position(4, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(8), new Position(4, 3)); + assert.deepStrictEqual(pieceTable.getPositionAt(9), new Position(5, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(11), new Position(6, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(12), new Position(6, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(13), new Position(6, 3)); - assert.equal(pieceTable.getOffsetAt(4, 1), 6); - assert.equal(pieceTable.getOffsetAt(4, 2), 7); - assert.equal(pieceTable.getOffsetAt(4, 3), 8); - assert.equal(pieceTable.getOffsetAt(5, 1), 9); - assert.equal(pieceTable.getOffsetAt(6, 1), 11); - assert.equal(pieceTable.getOffsetAt(6, 2), 12); - assert.equal(pieceTable.getOffsetAt(6, 3), 13); + assert.strictEqual(pieceTable.getOffsetAt(4, 1), 6); + assert.strictEqual(pieceTable.getOffsetAt(4, 2), 7); + assert.strictEqual(pieceTable.getOffsetAt(4, 3), 8); + assert.strictEqual(pieceTable.getOffsetAt(5, 1), 9); + assert.strictEqual(pieceTable.getOffsetAt(6, 1), 11); + assert.strictEqual(pieceTable.getOffsetAt(6, 2), 12); + assert.strictEqual(pieceTable.getOffsetAt(6, 3), 13); assertTreeInvariants(pieceTable); }); @@ -629,23 +629,23 @@ suite('prefix sum for line feed', () => { pieceTable.insert(8, 'fh\ni\njk'); pieceTable.delete(7, 2); - assert.equal(pieceTable.getLinesRawContent(), 'a\nb\nc\ndh\ni\njk'); - assert.equal(pieceTable.getLineCount(), 6); - assert.deepEqual(pieceTable.getPositionAt(6), new Position(4, 1)); - assert.deepEqual(pieceTable.getPositionAt(7), new Position(4, 2)); - assert.deepEqual(pieceTable.getPositionAt(8), new Position(4, 3)); - assert.deepEqual(pieceTable.getPositionAt(9), new Position(5, 1)); - assert.deepEqual(pieceTable.getPositionAt(11), new Position(6, 1)); - assert.deepEqual(pieceTable.getPositionAt(12), new Position(6, 2)); - assert.deepEqual(pieceTable.getPositionAt(13), new Position(6, 3)); + assert.strictEqual(pieceTable.getLinesRawContent(), 'a\nb\nc\ndh\ni\njk'); + assert.strictEqual(pieceTable.getLineCount(), 6); + assert.deepStrictEqual(pieceTable.getPositionAt(6), new Position(4, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(7), new Position(4, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(8), new Position(4, 3)); + assert.deepStrictEqual(pieceTable.getPositionAt(9), new Position(5, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(11), new Position(6, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(12), new Position(6, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(13), new Position(6, 3)); - assert.equal(pieceTable.getOffsetAt(4, 1), 6); - assert.equal(pieceTable.getOffsetAt(4, 2), 7); - assert.equal(pieceTable.getOffsetAt(4, 3), 8); - assert.equal(pieceTable.getOffsetAt(5, 1), 9); - assert.equal(pieceTable.getOffsetAt(6, 1), 11); - assert.equal(pieceTable.getOffsetAt(6, 2), 12); - assert.equal(pieceTable.getOffsetAt(6, 3), 13); + assert.strictEqual(pieceTable.getOffsetAt(4, 1), 6); + assert.strictEqual(pieceTable.getOffsetAt(4, 2), 7); + assert.strictEqual(pieceTable.getOffsetAt(4, 3), 8); + assert.strictEqual(pieceTable.getOffsetAt(5, 1), 9); + assert.strictEqual(pieceTable.getOffsetAt(6, 1), 11); + assert.strictEqual(pieceTable.getOffsetAt(6, 2), 12); + assert.strictEqual(pieceTable.getOffsetAt(6, 3), 13); assertTreeInvariants(pieceTable); }); @@ -661,7 +661,7 @@ suite('prefix sum for line feed', () => { str = str.substring(0, 14) + 'X ZZ\nYZZYZXXY Y XY\n ' + str.substring(14); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); assertTreeInvariants(pieceTable); }); @@ -675,7 +675,7 @@ suite('prefix sum for line feed', () => { pieceTable.insert(3, 'XXY \n\nY Y YYY ZYXY '); str = str.substring(0, 3) + 'XXY \n\nY Y YYY ZYXY ' + str.substring(3); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); assertTreeInvariants(pieceTable); }); @@ -803,12 +803,12 @@ suite('get text in range', () => { pieceTable.delete(7, 2); // 'a\nb\nc\ndh\ni\njk' - assert.equal(pieceTable.getValueInRange(new Range(1, 1, 1, 3)), 'a\n'); - assert.equal(pieceTable.getValueInRange(new Range(2, 1, 2, 3)), 'b\n'); - assert.equal(pieceTable.getValueInRange(new Range(3, 1, 3, 3)), 'c\n'); - assert.equal(pieceTable.getValueInRange(new Range(4, 1, 4, 4)), 'dh\n'); - assert.equal(pieceTable.getValueInRange(new Range(5, 1, 5, 3)), 'i\n'); - assert.equal(pieceTable.getValueInRange(new Range(6, 1, 6, 3)), 'jk'); + assert.strictEqual(pieceTable.getValueInRange(new Range(1, 1, 1, 3)), 'a\n'); + assert.strictEqual(pieceTable.getValueInRange(new Range(2, 1, 2, 3)), 'b\n'); + assert.strictEqual(pieceTable.getValueInRange(new Range(3, 1, 3, 3)), 'c\n'); + assert.strictEqual(pieceTable.getValueInRange(new Range(4, 1, 4, 4)), 'dh\n'); + assert.strictEqual(pieceTable.getValueInRange(new Range(5, 1, 5, 3)), 'i\n'); + assert.strictEqual(pieceTable.getValueInRange(new Range(6, 1, 6, 3)), 'jk'); assertTreeInvariants(pieceTable); }); @@ -902,18 +902,18 @@ suite('get text in range', () => { test('get line content', () => { let pieceTable = createTextBuffer(['1']); - assert.equal(pieceTable.getLineRawContent(1), '1'); + assert.strictEqual(pieceTable.getLineRawContent(1), '1'); pieceTable.insert(1, '2'); - assert.equal(pieceTable.getLineRawContent(1), '12'); + assert.strictEqual(pieceTable.getLineRawContent(1), '12'); assertTreeInvariants(pieceTable); }); test('get line content basic', () => { let pieceTable = createTextBuffer(['1\n2\n3\n4']); - assert.equal(pieceTable.getLineRawContent(1), '1\n'); - assert.equal(pieceTable.getLineRawContent(2), '2\n'); - assert.equal(pieceTable.getLineRawContent(3), '3\n'); - assert.equal(pieceTable.getLineRawContent(4), '4'); + assert.strictEqual(pieceTable.getLineRawContent(1), '1\n'); + assert.strictEqual(pieceTable.getLineRawContent(2), '2\n'); + assert.strictEqual(pieceTable.getLineRawContent(3), '3\n'); + assert.strictEqual(pieceTable.getLineRawContent(4), '4'); assertTreeInvariants(pieceTable); }); @@ -923,12 +923,12 @@ suite('get text in range', () => { pieceTable.delete(7, 2); // 'a\nb\nc\ndh\ni\njk' - assert.equal(pieceTable.getLineRawContent(1), 'a\n'); - assert.equal(pieceTable.getLineRawContent(2), 'b\n'); - assert.equal(pieceTable.getLineRawContent(3), 'c\n'); - assert.equal(pieceTable.getLineRawContent(4), 'dh\n'); - assert.equal(pieceTable.getLineRawContent(5), 'i\n'); - assert.equal(pieceTable.getLineRawContent(6), 'jk'); + assert.strictEqual(pieceTable.getLineRawContent(1), 'a\n'); + assert.strictEqual(pieceTable.getLineRawContent(2), 'b\n'); + assert.strictEqual(pieceTable.getLineRawContent(3), 'c\n'); + assert.strictEqual(pieceTable.getLineRawContent(4), 'dh\n'); + assert.strictEqual(pieceTable.getLineRawContent(5), 'i\n'); + assert.strictEqual(pieceTable.getLineRawContent(6), 'jk'); assertTreeInvariants(pieceTable); }); @@ -973,7 +973,7 @@ suite('CRLF', () => { pieceTable.insert(0, 'a\r\nb'); pieceTable.delete(0, 2); - assert.equal(pieceTable.getLineCount(), 2); + assert.strictEqual(pieceTable.getLineCount(), 2); assertTreeInvariants(pieceTable); }); @@ -982,7 +982,7 @@ suite('CRLF', () => { pieceTable.insert(0, 'a\r\nb'); pieceTable.delete(2, 2); - assert.equal(pieceTable.getLineCount(), 2); + assert.strictEqual(pieceTable.getLineCount(), 2); assertTreeInvariants(pieceTable); }); @@ -999,7 +999,7 @@ suite('CRLF', () => { str = str.substring(0, 2) + str.substring(2 + 3); let lines = splitLines(str); - assert.equal(pieceTable.getLineCount(), lines.length); + assert.strictEqual(pieceTable.getLineCount(), lines.length); assertTreeInvariants(pieceTable); }); test('random bug 2', () => { @@ -1014,7 +1014,7 @@ suite('CRLF', () => { str = str.substring(0, 4) + str.substring(4 + 1); let lines = splitLines(str); - assert.equal(pieceTable.getLineCount(), lines.length); + assert.strictEqual(pieceTable.getLineCount(), lines.length); assertTreeInvariants(pieceTable); }); test('random bug 3', () => { @@ -1035,7 +1035,7 @@ suite('CRLF', () => { str = str.substring(0, 3) + '\r\r\r\n' + str.substring(3); let lines = splitLines(str); - assert.equal(pieceTable.getLineCount(), lines.length); + assert.strictEqual(pieceTable.getLineCount(), lines.length); assertTreeInvariants(pieceTable); }); test('random bug 4', () => { @@ -1185,14 +1185,14 @@ suite('centralized lineStarts with CRLF', () => { test('delete CR in CRLF 1', () => { let pieceTable = createTextBuffer(['a\r\nb'], false); pieceTable.delete(2, 2); - assert.equal(pieceTable.getLineCount(), 2); + assert.strictEqual(pieceTable.getLineCount(), 2); assertTreeInvariants(pieceTable); }); test('delete CR in CRLF 2', () => { let pieceTable = createTextBuffer(['a\r\nb']); pieceTable.delete(0, 2); - assert.equal(pieceTable.getLineCount(), 2); + assert.strictEqual(pieceTable.getLineCount(), 2); assertTreeInvariants(pieceTable); }); @@ -1207,7 +1207,7 @@ suite('centralized lineStarts with CRLF', () => { str = str.substring(0, 2) + str.substring(2 + 3); let lines = splitLines(str); - assert.equal(pieceTable.getLineCount(), lines.length); + assert.strictEqual(pieceTable.getLineCount(), lines.length); assertTreeInvariants(pieceTable); }); test('random bug 2', () => { @@ -1220,7 +1220,7 @@ suite('centralized lineStarts with CRLF', () => { str = str.substring(0, 4) + str.substring(4 + 1); let lines = splitLines(str); - assert.equal(pieceTable.getLineCount(), lines.length); + assert.strictEqual(pieceTable.getLineCount(), lines.length); assertTreeInvariants(pieceTable); }); @@ -1240,7 +1240,7 @@ suite('centralized lineStarts with CRLF', () => { str = str.substring(0, 3) + '\r\r\r\n' + str.substring(3); let lines = splitLines(str); - assert.equal(pieceTable.getLineCount(), lines.length); + assert.strictEqual(pieceTable.getLineCount(), lines.length); assertTreeInvariants(pieceTable); }); @@ -1386,7 +1386,7 @@ suite('centralized lineStarts with CRLF', () => { pieceTable.insert(7, '\r\r\r\r'); str = str.substring(0, 7) + '\r\r\r\r' + str.substring(7); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); assertTreeInvariants(pieceTable); }); @@ -1407,7 +1407,7 @@ suite('centralized lineStarts with CRLF', () => { pieceTable.delete(11, 2); str = str.substring(0, 11) + str.substring(11 + 2); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); assertTreeInvariants(pieceTable); }); @@ -1424,7 +1424,7 @@ suite('centralized lineStarts with CRLF', () => { pieceTable.delete(1, 2); str = str.substring(0, 1) + str.substring(1 + 2); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); assertTreeInvariants(pieceTable); }); @@ -1437,7 +1437,7 @@ suite('centralized lineStarts with CRLF', () => { pieceTable.insert(3, '\r\n\n\n'); str = str.substring(0, 3) + '\r\n\n\n' + str.substring(3); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); assertTreeInvariants(pieceTable); }); @@ -1469,7 +1469,7 @@ suite('random is unsupervised', () => { pieceTable.insert(0, 'VZXXZYZX\r'); str = str.substring(0, 0) + 'VZXXZYZX\r' + str.substring(0); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); testLinesContent(str, pieceTable); @@ -1507,7 +1507,7 @@ suite('random is unsupervised', () => { } // console.log(output); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); testLinesContent(str, pieceTable); @@ -1543,7 +1543,7 @@ suite('random is unsupervised', () => { } } - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); testLinesContent(str, pieceTable); assertTreeInvariants(pieceTable); @@ -1577,7 +1577,7 @@ suite('random is unsupervised', () => { testLinesContent(str, pieceTable); } - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); testLinesContent(str, pieceTable); assertTreeInvariants(pieceTable); @@ -1612,33 +1612,33 @@ suite('buffer api', () => { test('getLineCharCode - issue #45735', () => { let pieceTable = createTextBuffer(['LINE1\nline2']); - assert.equal(pieceTable.getLineCharCode(1, 0), 'L'.charCodeAt(0), 'L'); - assert.equal(pieceTable.getLineCharCode(1, 1), 'I'.charCodeAt(0), 'I'); - assert.equal(pieceTable.getLineCharCode(1, 2), 'N'.charCodeAt(0), 'N'); - assert.equal(pieceTable.getLineCharCode(1, 3), 'E'.charCodeAt(0), 'E'); - assert.equal(pieceTable.getLineCharCode(1, 4), '1'.charCodeAt(0), '1'); - assert.equal(pieceTable.getLineCharCode(1, 5), '\n'.charCodeAt(0), '\\n'); - assert.equal(pieceTable.getLineCharCode(2, 0), 'l'.charCodeAt(0), 'l'); - assert.equal(pieceTable.getLineCharCode(2, 1), 'i'.charCodeAt(0), 'i'); - assert.equal(pieceTable.getLineCharCode(2, 2), 'n'.charCodeAt(0), 'n'); - assert.equal(pieceTable.getLineCharCode(2, 3), 'e'.charCodeAt(0), 'e'); - assert.equal(pieceTable.getLineCharCode(2, 4), '2'.charCodeAt(0), '2'); + assert.strictEqual(pieceTable.getLineCharCode(1, 0), 'L'.charCodeAt(0), 'L'); + assert.strictEqual(pieceTable.getLineCharCode(1, 1), 'I'.charCodeAt(0), 'I'); + assert.strictEqual(pieceTable.getLineCharCode(1, 2), 'N'.charCodeAt(0), 'N'); + assert.strictEqual(pieceTable.getLineCharCode(1, 3), 'E'.charCodeAt(0), 'E'); + assert.strictEqual(pieceTable.getLineCharCode(1, 4), '1'.charCodeAt(0), '1'); + assert.strictEqual(pieceTable.getLineCharCode(1, 5), '\n'.charCodeAt(0), '\\n'); + assert.strictEqual(pieceTable.getLineCharCode(2, 0), 'l'.charCodeAt(0), 'l'); + assert.strictEqual(pieceTable.getLineCharCode(2, 1), 'i'.charCodeAt(0), 'i'); + assert.strictEqual(pieceTable.getLineCharCode(2, 2), 'n'.charCodeAt(0), 'n'); + assert.strictEqual(pieceTable.getLineCharCode(2, 3), 'e'.charCodeAt(0), 'e'); + assert.strictEqual(pieceTable.getLineCharCode(2, 4), '2'.charCodeAt(0), '2'); }); test('getLineCharCode - issue #47733', () => { let pieceTable = createTextBuffer(['', 'LINE1\n', 'line2']); - assert.equal(pieceTable.getLineCharCode(1, 0), 'L'.charCodeAt(0), 'L'); - assert.equal(pieceTable.getLineCharCode(1, 1), 'I'.charCodeAt(0), 'I'); - assert.equal(pieceTable.getLineCharCode(1, 2), 'N'.charCodeAt(0), 'N'); - assert.equal(pieceTable.getLineCharCode(1, 3), 'E'.charCodeAt(0), 'E'); - assert.equal(pieceTable.getLineCharCode(1, 4), '1'.charCodeAt(0), '1'); - assert.equal(pieceTable.getLineCharCode(1, 5), '\n'.charCodeAt(0), '\\n'); - assert.equal(pieceTable.getLineCharCode(2, 0), 'l'.charCodeAt(0), 'l'); - assert.equal(pieceTable.getLineCharCode(2, 1), 'i'.charCodeAt(0), 'i'); - assert.equal(pieceTable.getLineCharCode(2, 2), 'n'.charCodeAt(0), 'n'); - assert.equal(pieceTable.getLineCharCode(2, 3), 'e'.charCodeAt(0), 'e'); - assert.equal(pieceTable.getLineCharCode(2, 4), '2'.charCodeAt(0), '2'); + assert.strictEqual(pieceTable.getLineCharCode(1, 0), 'L'.charCodeAt(0), 'L'); + assert.strictEqual(pieceTable.getLineCharCode(1, 1), 'I'.charCodeAt(0), 'I'); + assert.strictEqual(pieceTable.getLineCharCode(1, 2), 'N'.charCodeAt(0), 'N'); + assert.strictEqual(pieceTable.getLineCharCode(1, 3), 'E'.charCodeAt(0), 'E'); + assert.strictEqual(pieceTable.getLineCharCode(1, 4), '1'.charCodeAt(0), '1'); + assert.strictEqual(pieceTable.getLineCharCode(1, 5), '\n'.charCodeAt(0), '\\n'); + assert.strictEqual(pieceTable.getLineCharCode(2, 0), 'l'.charCodeAt(0), 'l'); + assert.strictEqual(pieceTable.getLineCharCode(2, 1), 'i'.charCodeAt(0), 'i'); + assert.strictEqual(pieceTable.getLineCharCode(2, 2), 'n'.charCodeAt(0), 'n'); + assert.strictEqual(pieceTable.getLineCharCode(2, 3), 'e'.charCodeAt(0), 'e'); + assert.strictEqual(pieceTable.getLineCharCode(2, 4), '2'.charCodeAt(0), '2'); }); }); @@ -1771,7 +1771,7 @@ suite('snapshot', () => { ]); const snapshot = model.createSnapshot(); const snapshot1 = model.createSnapshot(); - assert.equal(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); + assert.strictEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); model.applyEdits([ { @@ -1786,7 +1786,7 @@ suite('snapshot', () => { } ]); - assert.equal(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot1)); + assert.strictEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot1)); }); test('immutable snapshot 1', () => { @@ -1806,7 +1806,7 @@ suite('snapshot', () => { } ]); - assert.equal(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); + assert.strictEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); }); test('immutable snapshot 2', () => { @@ -1826,7 +1826,7 @@ suite('snapshot', () => { } ]); - assert.equal(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); + assert.strictEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); }); test('immutable snapshot 3', () => { @@ -1845,7 +1845,7 @@ suite('snapshot', () => { } ]); - assert.notEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); + assert.notStrictEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); }); }); @@ -1854,7 +1854,7 @@ suite('chunk based search', () => { let pieceTree = createTextBuffer(['']); pieceTree.delete(0, 1); let ret = pieceTree.findMatchesLineByLine(new Range(1, 1, 1, 1), new SearchData(/abc/, new WordCharacterClassifier(',./'), 'abc'), true, 1000); - assert.equal(ret.length, 0); + assert.strictEqual(ret.length, 0); }); test('#45770. FindInNode should not cross node boundary.', () => { @@ -1873,11 +1873,11 @@ suite('chunk based search', () => { pieceTree.insert(16, ' '); let ret = pieceTree.findMatchesLineByLine(new Range(1, 1, 4, 13), new SearchData(/\[/gi, new WordCharacterClassifier(',./'), '['), true, 1000); - assert.equal(ret.length, 3); + assert.strictEqual(ret.length, 3); - assert.deepEqual(ret[0].range, new Range(2, 3, 2, 4)); - assert.deepEqual(ret[1].range, new Range(3, 3, 3, 4)); - assert.deepEqual(ret[2].range, new Range(4, 3, 4, 4)); + assert.deepStrictEqual(ret[0].range, new Range(2, 3, 2, 4)); + assert.deepStrictEqual(ret[1].range, new Range(3, 3, 3, 4)); + assert.deepStrictEqual(ret[2].range, new Range(4, 3, 4, 4)); }); test('search searching from the middle', () => { @@ -1889,12 +1889,12 @@ suite('chunk based search', () => { ]); pieceTree.delete(4, 1); let ret = pieceTree.findMatchesLineByLine(new Range(2, 3, 2, 6), new SearchData(/a/gi, null, 'a'), true, 1000); - assert.equal(ret.length, 1); - assert.deepEqual(ret[0].range, new Range(2, 3, 2, 4)); + assert.strictEqual(ret.length, 1); + assert.deepStrictEqual(ret[0].range, new Range(2, 3, 2, 4)); pieceTree.delete(4, 1); ret = pieceTree.findMatchesLineByLine(new Range(2, 2, 2, 5), new SearchData(/a/gi, null, 'a'), true, 1000); - assert.equal(ret.length, 1); - assert.deepEqual(ret[0].range, new Range(2, 2, 2, 3)); + assert.strictEqual(ret.length, 1); + assert.deepStrictEqual(ret[0].range, new Range(2, 2, 2, 3)); }); }); diff --git a/src/vs/editor/test/common/model/textChange.test.ts b/src/vs/editor/test/common/model/textChange.test.ts index d6d42dfc..bbc29ea5 100644 --- a/src/vs/editor/test/common/model/textChange.test.ts +++ b/src/vs/editor/test/common/model/textChange.test.ts @@ -75,7 +75,7 @@ suite('TextChangeCompressor', () => { }; }); let actualDoResult = getResultingContent(initialText, compressedDoTextEdits); - assert.equal(actualDoResult, finalText); + assert.strictEqual(actualDoResult, finalText); let compressedUndoTextEdits: IGeneratedEdit[] = compressedTextChanges.map((change) => { return { @@ -85,7 +85,7 @@ suite('TextChangeCompressor', () => { }; }); let actualUndoResult = getResultingContent(finalText, compressedUndoTextEdits); - assert.equal(actualUndoResult, initialText); + assert.strictEqual(actualUndoResult, initialText); } test('simple 1', () => { diff --git a/src/vs/editor/test/common/model/textModel.test.ts b/src/vs/editor/test/common/model/textModel.test.ts index 2da33417..2d8e6bbd 100644 --- a/src/vs/editor/test/common/model/textModel.test.ts +++ b/src/vs/editor/test/common/model/textModel.test.ts @@ -22,8 +22,8 @@ function testGuessIndentation(defaultInsertSpaces: boolean, defaultTabSize: numb let r = m.getOptions(); m.dispose(); - assert.equal(r.insertSpaces, expectedInsertSpaces, msg); - assert.equal(r.tabSize, expectedTabSize, msg); + assert.strictEqual(r.insertSpaces, expectedInsertSpaces, msg); + assert.strictEqual(r.tabSize, expectedTabSize, msg); } function assertGuess(expectedInsertSpaces: boolean | undefined, expectedTabSize: number | undefined | [number], text: string[], msg?: string): void { @@ -75,14 +75,14 @@ suite('TextModelData.fromString', () => { } function testTextModelDataFromString(text: string, expected: ITextBufferData): void { - const textBuffer = createTextBuffer(text, TextModel.DEFAULT_CREATION_OPTIONS.defaultEOL); + const textBuffer = createTextBuffer(text, TextModel.DEFAULT_CREATION_OPTIONS.defaultEOL).textBuffer; let actual: ITextBufferData = { EOL: textBuffer.getEOL(), lines: textBuffer.getLinesContent(), containsRTL: textBuffer.mightContainRTL(), isBasicASCII: !textBuffer.mightContainNonBasicASCII() }; - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } test('one line text', () => { @@ -164,30 +164,30 @@ suite('Editor Model - TextModel', () => { test('getValueLengthInRange', () => { let m = createTextModel('My First Line\r\nMy Second Line\r\nMy Third Line'); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 14)), 'My First Line'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 2, 1)), 'My First Line\r\n'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 2, 1)), 'y First Line\r\n'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 2, 2)), 'y First Line\r\nM'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 2, 1000)), 'y First Line\r\nMy Second Line'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 3, 1)), 'y First Line\r\nMy Second Line\r\n'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 3, 1000)), 'y First Line\r\nMy Second Line\r\nMy Third Line'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1000, 1000)), 'My First Line\r\nMy Second Line\r\nMy Third Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1, 14)), 'My First Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 2, 1)), 'My First Line\r\n'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 2, 1)), 'y First Line\r\n'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 2, 2)), 'y First Line\r\nM'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 2, 1000)), 'y First Line\r\nMy Second Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 3, 1)), 'y First Line\r\nMy Second Line\r\n'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 3, 1000)), 'y First Line\r\nMy Second Line\r\nMy Third Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1000, 1000)), 'My First Line\r\nMy Second Line\r\nMy Third Line'.length); m = createTextModel('My First Line\nMy Second Line\nMy Third Line'); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 14)), 'My First Line'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 2, 1)), 'My First Line\n'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 2, 1)), 'y First Line\n'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 2, 2)), 'y First Line\nM'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 2, 1000)), 'y First Line\nMy Second Line'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 3, 1)), 'y First Line\nMy Second Line\n'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 3, 1000)), 'y First Line\nMy Second Line\nMy Third Line'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1000, 1000)), 'My First Line\nMy Second Line\nMy Third Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1, 14)), 'My First Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 2, 1)), 'My First Line\n'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 2, 1)), 'y First Line\n'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 2, 2)), 'y First Line\nM'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 2, 1000)), 'y First Line\nMy Second Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 3, 1)), 'y First Line\nMy Second Line\n'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 3, 1000)), 'y First Line\nMy Second Line\nMy Third Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1000, 1000)), 'My First Line\nMy Second Line\nMy Third Line'.length); }); test('guess indentation 1', () => { @@ -664,69 +664,69 @@ suite('Editor Model - TextModel', () => { let m = createTextModel('line one\nline two'); - assert.deepEqual(m.validatePosition(new Position(0, 0)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(0, 1)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(0, 0)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(0, 1)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(1, 1)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(1, 2)), new Position(1, 2)); - assert.deepEqual(m.validatePosition(new Position(1, 30)), new Position(1, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 1)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 2)), new Position(1, 2)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 30)), new Position(1, 9)); - assert.deepEqual(m.validatePosition(new Position(2, 0)), new Position(2, 1)); - assert.deepEqual(m.validatePosition(new Position(2, 1)), new Position(2, 1)); - assert.deepEqual(m.validatePosition(new Position(2, 2)), new Position(2, 2)); - assert.deepEqual(m.validatePosition(new Position(2, 30)), new Position(2, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 0)), new Position(2, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 1)), new Position(2, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 2)), new Position(2, 2)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 30)), new Position(2, 9)); - assert.deepEqual(m.validatePosition(new Position(3, 0)), new Position(2, 9)); - assert.deepEqual(m.validatePosition(new Position(3, 1)), new Position(2, 9)); - assert.deepEqual(m.validatePosition(new Position(3, 30)), new Position(2, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(3, 0)), new Position(2, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(3, 1)), new Position(2, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(3, 30)), new Position(2, 9)); - assert.deepEqual(m.validatePosition(new Position(30, 30)), new Position(2, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(30, 30)), new Position(2, 9)); - assert.deepEqual(m.validatePosition(new Position(-123.123, -0.5)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(Number.MIN_VALUE, Number.MIN_VALUE)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(-123.123, -0.5)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(Number.MIN_VALUE, Number.MIN_VALUE)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(Number.MAX_VALUE, Number.MAX_VALUE)), new Position(2, 9)); - assert.deepEqual(m.validatePosition(new Position(123.23, 47.5)), new Position(2, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(Number.MAX_VALUE, Number.MAX_VALUE)), new Position(2, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(123.23, 47.5)), new Position(2, 9)); }); test('validatePosition around high-low surrogate pairs 1', () => { let m = createTextModel('a📚b'); - assert.deepEqual(m.validatePosition(new Position(0, 0)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(0, 1)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(0, 7)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(0, 0)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(0, 1)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(0, 7)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(1, 1)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(1, 2)), new Position(1, 2)); - assert.deepEqual(m.validatePosition(new Position(1, 3)), new Position(1, 2)); - assert.deepEqual(m.validatePosition(new Position(1, 4)), new Position(1, 4)); - assert.deepEqual(m.validatePosition(new Position(1, 5)), new Position(1, 5)); - assert.deepEqual(m.validatePosition(new Position(1, 30)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 1)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 2)), new Position(1, 2)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 3)), new Position(1, 2)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 4)), new Position(1, 4)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 5)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 30)), new Position(1, 5)); - assert.deepEqual(m.validatePosition(new Position(2, 0)), new Position(1, 5)); - assert.deepEqual(m.validatePosition(new Position(2, 1)), new Position(1, 5)); - assert.deepEqual(m.validatePosition(new Position(2, 2)), new Position(1, 5)); - assert.deepEqual(m.validatePosition(new Position(2, 30)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 0)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 1)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 2)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 30)), new Position(1, 5)); - assert.deepEqual(m.validatePosition(new Position(-123.123, -0.5)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(Number.MIN_VALUE, Number.MIN_VALUE)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(-123.123, -0.5)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(Number.MIN_VALUE, Number.MIN_VALUE)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(Number.MAX_VALUE, Number.MAX_VALUE)), new Position(1, 5)); - assert.deepEqual(m.validatePosition(new Position(123.23, 47.5)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(Number.MAX_VALUE, Number.MAX_VALUE)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(123.23, 47.5)), new Position(1, 5)); }); test('validatePosition around high-low surrogate pairs 2', () => { let m = createTextModel('a📚📚b'); - assert.deepEqual(m.validatePosition(new Position(1, 1)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(1, 2)), new Position(1, 2)); - assert.deepEqual(m.validatePosition(new Position(1, 3)), new Position(1, 2)); - assert.deepEqual(m.validatePosition(new Position(1, 4)), new Position(1, 4)); - assert.deepEqual(m.validatePosition(new Position(1, 5)), new Position(1, 4)); - assert.deepEqual(m.validatePosition(new Position(1, 6)), new Position(1, 6)); - assert.deepEqual(m.validatePosition(new Position(1, 7)), new Position(1, 7)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 1)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 2)), new Position(1, 2)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 3)), new Position(1, 2)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 4)), new Position(1, 4)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 5)), new Position(1, 4)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 6)), new Position(1, 6)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 7)), new Position(1, 7)); }); @@ -734,133 +734,133 @@ suite('Editor Model - TextModel', () => { let m = createTextModel('line one\nline two'); - assert.deepEqual(m.validatePosition(new Position(NaN, 1)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(1, NaN)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(NaN, 1)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(1, NaN)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(NaN, NaN)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(2, NaN)), new Position(2, 1)); - assert.deepEqual(m.validatePosition(new Position(NaN, 3)), new Position(1, 3)); + assert.deepStrictEqual(m.validatePosition(new Position(NaN, NaN)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(2, NaN)), new Position(2, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(NaN, 3)), new Position(1, 3)); }); test('issue #71480: validatePosition handle floats', () => { let m = createTextModel('line one\nline two'); - assert.deepEqual(m.validatePosition(new Position(0.2, 1)), new Position(1, 1), 'a'); - assert.deepEqual(m.validatePosition(new Position(1.2, 1)), new Position(1, 1), 'b'); - assert.deepEqual(m.validatePosition(new Position(1.5, 2)), new Position(1, 2), 'c'); - assert.deepEqual(m.validatePosition(new Position(1.8, 3)), new Position(1, 3), 'd'); - assert.deepEqual(m.validatePosition(new Position(1, 0.3)), new Position(1, 1), 'e'); - assert.deepEqual(m.validatePosition(new Position(2, 0.8)), new Position(2, 1), 'f'); - assert.deepEqual(m.validatePosition(new Position(1, 1.2)), new Position(1, 1), 'g'); - assert.deepEqual(m.validatePosition(new Position(2, 1.5)), new Position(2, 1), 'h'); + assert.deepStrictEqual(m.validatePosition(new Position(0.2, 1)), new Position(1, 1), 'a'); + assert.deepStrictEqual(m.validatePosition(new Position(1.2, 1)), new Position(1, 1), 'b'); + assert.deepStrictEqual(m.validatePosition(new Position(1.5, 2)), new Position(1, 2), 'c'); + assert.deepStrictEqual(m.validatePosition(new Position(1.8, 3)), new Position(1, 3), 'd'); + assert.deepStrictEqual(m.validatePosition(new Position(1, 0.3)), new Position(1, 1), 'e'); + assert.deepStrictEqual(m.validatePosition(new Position(2, 0.8)), new Position(2, 1), 'f'); + assert.deepStrictEqual(m.validatePosition(new Position(1, 1.2)), new Position(1, 1), 'g'); + assert.deepStrictEqual(m.validatePosition(new Position(2, 1.5)), new Position(2, 1), 'h'); }); test('issue #71480: validateRange handle floats', () => { let m = createTextModel('line one\nline two'); - assert.deepEqual(m.validateRange(new Range(0.2, 1.5, 0.8, 2.5)), new Range(1, 1, 1, 1)); - assert.deepEqual(m.validateRange(new Range(1.2, 1.7, 1.8, 2.2)), new Range(1, 1, 1, 2)); + assert.deepStrictEqual(m.validateRange(new Range(0.2, 1.5, 0.8, 2.5)), new Range(1, 1, 1, 1)); + assert.deepStrictEqual(m.validateRange(new Range(1.2, 1.7, 1.8, 2.2)), new Range(1, 1, 1, 2)); }); test('validateRange around high-low surrogate pairs 1', () => { let m = createTextModel('a📚b'); - assert.deepEqual(m.validateRange(new Range(0, 0, 0, 1)), new Range(1, 1, 1, 1)); - assert.deepEqual(m.validateRange(new Range(0, 0, 0, 7)), new Range(1, 1, 1, 1)); + assert.deepStrictEqual(m.validateRange(new Range(0, 0, 0, 1)), new Range(1, 1, 1, 1)); + assert.deepStrictEqual(m.validateRange(new Range(0, 0, 0, 7)), new Range(1, 1, 1, 1)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 1)), new Range(1, 1, 1, 1)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 2)), new Range(1, 1, 1, 2)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 3)), new Range(1, 1, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 4)), new Range(1, 1, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 5)), new Range(1, 1, 1, 5)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 1)), new Range(1, 1, 1, 1)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 2)), new Range(1, 1, 1, 2)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 3)), new Range(1, 1, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 4)), new Range(1, 1, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 5)), new Range(1, 1, 1, 5)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 2)), new Range(1, 2, 1, 2)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 3)), new Range(1, 2, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 4)), new Range(1, 2, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 5)), new Range(1, 2, 1, 5)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 2)), new Range(1, 2, 1, 2)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 3)), new Range(1, 2, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 4)), new Range(1, 2, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 5)), new Range(1, 2, 1, 5)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 3)), new Range(1, 2, 1, 2)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 4)), new Range(1, 2, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 5)), new Range(1, 2, 1, 5)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 3)), new Range(1, 2, 1, 2)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 4)), new Range(1, 2, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 5)), new Range(1, 2, 1, 5)); - assert.deepEqual(m.validateRange(new Range(1, 4, 1, 4)), new Range(1, 4, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 4, 1, 5)), new Range(1, 4, 1, 5)); + assert.deepStrictEqual(m.validateRange(new Range(1, 4, 1, 4)), new Range(1, 4, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 4, 1, 5)), new Range(1, 4, 1, 5)); - assert.deepEqual(m.validateRange(new Range(1, 5, 1, 5)), new Range(1, 5, 1, 5)); + assert.deepStrictEqual(m.validateRange(new Range(1, 5, 1, 5)), new Range(1, 5, 1, 5)); }); test('validateRange around high-low surrogate pairs 2', () => { let m = createTextModel('a📚📚b'); - assert.deepEqual(m.validateRange(new Range(0, 0, 0, 1)), new Range(1, 1, 1, 1)); - assert.deepEqual(m.validateRange(new Range(0, 0, 0, 7)), new Range(1, 1, 1, 1)); + assert.deepStrictEqual(m.validateRange(new Range(0, 0, 0, 1)), new Range(1, 1, 1, 1)); + assert.deepStrictEqual(m.validateRange(new Range(0, 0, 0, 7)), new Range(1, 1, 1, 1)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 1)), new Range(1, 1, 1, 1)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 2)), new Range(1, 1, 1, 2)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 3)), new Range(1, 1, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 4)), new Range(1, 1, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 5)), new Range(1, 1, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 6)), new Range(1, 1, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 7)), new Range(1, 1, 1, 7)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 1)), new Range(1, 1, 1, 1)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 2)), new Range(1, 1, 1, 2)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 3)), new Range(1, 1, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 4)), new Range(1, 1, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 5)), new Range(1, 1, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 6)), new Range(1, 1, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 7)), new Range(1, 1, 1, 7)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 2)), new Range(1, 2, 1, 2)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 3)), new Range(1, 2, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 4)), new Range(1, 2, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 5)), new Range(1, 2, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 6)), new Range(1, 2, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 7)), new Range(1, 2, 1, 7)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 2)), new Range(1, 2, 1, 2)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 3)), new Range(1, 2, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 4)), new Range(1, 2, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 5)), new Range(1, 2, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 6)), new Range(1, 2, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 7)), new Range(1, 2, 1, 7)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 3)), new Range(1, 2, 1, 2)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 4)), new Range(1, 2, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 5)), new Range(1, 2, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 6)), new Range(1, 2, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 7)), new Range(1, 2, 1, 7)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 3)), new Range(1, 2, 1, 2)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 4)), new Range(1, 2, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 5)), new Range(1, 2, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 6)), new Range(1, 2, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 7)), new Range(1, 2, 1, 7)); - assert.deepEqual(m.validateRange(new Range(1, 4, 1, 4)), new Range(1, 4, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 4, 1, 5)), new Range(1, 4, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 4, 1, 6)), new Range(1, 4, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 4, 1, 7)), new Range(1, 4, 1, 7)); + assert.deepStrictEqual(m.validateRange(new Range(1, 4, 1, 4)), new Range(1, 4, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 4, 1, 5)), new Range(1, 4, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 4, 1, 6)), new Range(1, 4, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 4, 1, 7)), new Range(1, 4, 1, 7)); - assert.deepEqual(m.validateRange(new Range(1, 5, 1, 5)), new Range(1, 4, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 5, 1, 6)), new Range(1, 4, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 5, 1, 7)), new Range(1, 4, 1, 7)); + assert.deepStrictEqual(m.validateRange(new Range(1, 5, 1, 5)), new Range(1, 4, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 5, 1, 6)), new Range(1, 4, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 5, 1, 7)), new Range(1, 4, 1, 7)); - assert.deepEqual(m.validateRange(new Range(1, 6, 1, 6)), new Range(1, 6, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 6, 1, 7)), new Range(1, 6, 1, 7)); + assert.deepStrictEqual(m.validateRange(new Range(1, 6, 1, 6)), new Range(1, 6, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 6, 1, 7)), new Range(1, 6, 1, 7)); - assert.deepEqual(m.validateRange(new Range(1, 7, 1, 7)), new Range(1, 7, 1, 7)); + assert.deepStrictEqual(m.validateRange(new Range(1, 7, 1, 7)), new Range(1, 7, 1, 7)); }); test('modifyPosition', () => { let m = createTextModel('line one\nline two'); - assert.deepEqual(m.modifyPosition(new Position(1, 1), 0), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(0, 0), 0), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(30, 1), 0), new Position(2, 9)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 1), 0), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(0, 0), 0), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(30, 1), 0), new Position(2, 9)); - assert.deepEqual(m.modifyPosition(new Position(1, 1), 17), new Position(2, 9)); - assert.deepEqual(m.modifyPosition(new Position(1, 1), 1), new Position(1, 2)); - assert.deepEqual(m.modifyPosition(new Position(1, 1), 3), new Position(1, 4)); - assert.deepEqual(m.modifyPosition(new Position(1, 2), 10), new Position(2, 3)); - assert.deepEqual(m.modifyPosition(new Position(1, 5), 13), new Position(2, 9)); - assert.deepEqual(m.modifyPosition(new Position(1, 2), 16), new Position(2, 9)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 1), 17), new Position(2, 9)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 1), 1), new Position(1, 2)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 1), 3), new Position(1, 4)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), 10), new Position(2, 3)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 5), 13), new Position(2, 9)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), 16), new Position(2, 9)); - assert.deepEqual(m.modifyPosition(new Position(2, 9), -17), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(1, 2), -1), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(1, 4), -3), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(2, 3), -10), new Position(1, 2)); - assert.deepEqual(m.modifyPosition(new Position(2, 9), -13), new Position(1, 5)); - assert.deepEqual(m.modifyPosition(new Position(2, 9), -16), new Position(1, 2)); + assert.deepStrictEqual(m.modifyPosition(new Position(2, 9), -17), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), -1), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 4), -3), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(2, 3), -10), new Position(1, 2)); + assert.deepStrictEqual(m.modifyPosition(new Position(2, 9), -13), new Position(1, 5)); + assert.deepStrictEqual(m.modifyPosition(new Position(2, 9), -16), new Position(1, 2)); - assert.deepEqual(m.modifyPosition(new Position(1, 2), 17), new Position(2, 9)); - assert.deepEqual(m.modifyPosition(new Position(1, 2), 100), new Position(2, 9)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), 17), new Position(2, 9)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), 100), new Position(2, 9)); - assert.deepEqual(m.modifyPosition(new Position(1, 2), -2), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(1, 2), -100), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(2, 2), -100), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(2, 9), -18), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), -2), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), -100), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(2, 2), -100), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(2, 9), -18), new Position(1, 1)); }); test('normalizeIndentation 1', () => { @@ -870,27 +870,27 @@ suite('Editor Model - TextModel', () => { } ); - assert.equal(model.normalizeIndentation('\t'), '\t'); - assert.equal(model.normalizeIndentation(' '), '\t'); - assert.equal(model.normalizeIndentation(' '), ' '); - assert.equal(model.normalizeIndentation(' '), ' '); - assert.equal(model.normalizeIndentation(' '), ' '); - assert.equal(model.normalizeIndentation(''), ''); - assert.equal(model.normalizeIndentation(' \t '), '\t\t'); - assert.equal(model.normalizeIndentation(' \t '), '\t '); - assert.equal(model.normalizeIndentation(' \t '), '\t '); - assert.equal(model.normalizeIndentation(' \t'), '\t '); + assert.strictEqual(model.normalizeIndentation('\t'), '\t'); + assert.strictEqual(model.normalizeIndentation(' '), '\t'); + assert.strictEqual(model.normalizeIndentation(' '), ' '); + assert.strictEqual(model.normalizeIndentation(' '), ' '); + assert.strictEqual(model.normalizeIndentation(' '), ' '); + assert.strictEqual(model.normalizeIndentation(''), ''); + assert.strictEqual(model.normalizeIndentation(' \t '), '\t\t'); + assert.strictEqual(model.normalizeIndentation(' \t '), '\t '); + assert.strictEqual(model.normalizeIndentation(' \t '), '\t '); + assert.strictEqual(model.normalizeIndentation(' \t'), '\t '); - assert.equal(model.normalizeIndentation('\ta'), '\ta'); - assert.equal(model.normalizeIndentation(' a'), '\ta'); - assert.equal(model.normalizeIndentation(' a'), ' a'); - assert.equal(model.normalizeIndentation(' a'), ' a'); - assert.equal(model.normalizeIndentation(' a'), ' a'); - assert.equal(model.normalizeIndentation('a'), 'a'); - assert.equal(model.normalizeIndentation(' \t a'), '\t\ta'); - assert.equal(model.normalizeIndentation(' \t a'), '\t a'); - assert.equal(model.normalizeIndentation(' \t a'), '\t a'); - assert.equal(model.normalizeIndentation(' \ta'), '\t a'); + assert.strictEqual(model.normalizeIndentation('\ta'), '\ta'); + assert.strictEqual(model.normalizeIndentation(' a'), '\ta'); + assert.strictEqual(model.normalizeIndentation(' a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' a'), ' a'); + assert.strictEqual(model.normalizeIndentation('a'), 'a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), '\t\ta'); + assert.strictEqual(model.normalizeIndentation(' \t a'), '\t a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), '\t a'); + assert.strictEqual(model.normalizeIndentation(' \ta'), '\t a'); model.dispose(); }); @@ -898,16 +898,16 @@ suite('Editor Model - TextModel', () => { test('normalizeIndentation 2', () => { let model = createTextModel(''); - assert.equal(model.normalizeIndentation('\ta'), ' a'); - assert.equal(model.normalizeIndentation(' a'), ' a'); - assert.equal(model.normalizeIndentation(' a'), ' a'); - assert.equal(model.normalizeIndentation(' a'), ' a'); - assert.equal(model.normalizeIndentation(' a'), ' a'); - assert.equal(model.normalizeIndentation('a'), 'a'); - assert.equal(model.normalizeIndentation(' \t a'), ' a'); - assert.equal(model.normalizeIndentation(' \t a'), ' a'); - assert.equal(model.normalizeIndentation(' \t a'), ' a'); - assert.equal(model.normalizeIndentation(' \ta'), ' a'); + assert.strictEqual(model.normalizeIndentation('\ta'), ' a'); + assert.strictEqual(model.normalizeIndentation(' a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' a'), ' a'); + assert.strictEqual(model.normalizeIndentation('a'), 'a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' \ta'), ' a'); model.dispose(); }); @@ -928,18 +928,18 @@ suite('Editor Model - TextModel', () => { '' ].join('\n')); - assert.equal(model.getLineFirstNonWhitespaceColumn(1), 1, '1'); - assert.equal(model.getLineFirstNonWhitespaceColumn(2), 2, '2'); - assert.equal(model.getLineFirstNonWhitespaceColumn(3), 2, '3'); - assert.equal(model.getLineFirstNonWhitespaceColumn(4), 3, '4'); - assert.equal(model.getLineFirstNonWhitespaceColumn(5), 3, '5'); - assert.equal(model.getLineFirstNonWhitespaceColumn(6), 0, '6'); - assert.equal(model.getLineFirstNonWhitespaceColumn(7), 0, '7'); - assert.equal(model.getLineFirstNonWhitespaceColumn(8), 0, '8'); - assert.equal(model.getLineFirstNonWhitespaceColumn(9), 0, '9'); - assert.equal(model.getLineFirstNonWhitespaceColumn(10), 4, '10'); - assert.equal(model.getLineFirstNonWhitespaceColumn(11), 0, '11'); - assert.equal(model.getLineFirstNonWhitespaceColumn(12), 0, '12'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(1), 1, '1'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(2), 2, '2'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(3), 2, '3'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(4), 3, '4'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(5), 3, '5'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(6), 0, '6'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(7), 0, '7'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(8), 0, '8'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(9), 0, '9'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(10), 4, '10'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(11), 0, '11'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(12), 0, '12'); }); test('getLineLastNonWhitespaceColumn', () => { @@ -958,24 +958,24 @@ suite('Editor Model - TextModel', () => { '' ].join('\n')); - assert.equal(model.getLineLastNonWhitespaceColumn(1), 4, '1'); - assert.equal(model.getLineLastNonWhitespaceColumn(2), 4, '2'); - assert.equal(model.getLineLastNonWhitespaceColumn(3), 4, '3'); - assert.equal(model.getLineLastNonWhitespaceColumn(4), 4, '4'); - assert.equal(model.getLineLastNonWhitespaceColumn(5), 4, '5'); - assert.equal(model.getLineLastNonWhitespaceColumn(6), 0, '6'); - assert.equal(model.getLineLastNonWhitespaceColumn(7), 0, '7'); - assert.equal(model.getLineLastNonWhitespaceColumn(8), 0, '8'); - assert.equal(model.getLineLastNonWhitespaceColumn(9), 0, '9'); - assert.equal(model.getLineLastNonWhitespaceColumn(10), 4, '10'); - assert.equal(model.getLineLastNonWhitespaceColumn(11), 0, '11'); - assert.equal(model.getLineLastNonWhitespaceColumn(12), 0, '12'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(1), 4, '1'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(2), 4, '2'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(3), 4, '3'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(4), 4, '4'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(5), 4, '5'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(6), 0, '6'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(7), 0, '7'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(8), 0, '8'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(9), 0, '9'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(10), 4, '10'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(11), 0, '11'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(12), 0, '12'); }); test('#50471. getValueInRange with invalid range', () => { let m = createTextModel('My First Line\r\nMy Second Line\r\nMy Third Line'); - assert.equal(m.getValueInRange(new Range(1, NaN, 1, 3)), 'My'); - assert.equal(m.getValueInRange(new Range(NaN, NaN, NaN, NaN)), ''); + assert.strictEqual(m.getValueInRange(new Range(1, NaN, 1, 3)), 'My'); + assert.strictEqual(m.getValueInRange(new Range(NaN, NaN, NaN, NaN)), ''); }); }); @@ -983,26 +983,26 @@ suite('TextModel.mightContainRTL', () => { test('nope', () => { let model = createTextModel('hello world!'); - assert.equal(model.mightContainRTL(), false); + assert.strictEqual(model.mightContainRTL(), false); }); test('yes', () => { let model = createTextModel('Hello,\nזוהי עובדה מבוססת שדעתו'); - assert.equal(model.mightContainRTL(), true); + assert.strictEqual(model.mightContainRTL(), true); }); test('setValue resets 1', () => { let model = createTextModel('hello world!'); - assert.equal(model.mightContainRTL(), false); + assert.strictEqual(model.mightContainRTL(), false); model.setValue('Hello,\nזוהי עובדה מבוססת שדעתו'); - assert.equal(model.mightContainRTL(), true); + assert.strictEqual(model.mightContainRTL(), true); }); test('setValue resets 2', () => { let model = createTextModel('Hello,\nهناك حقيقة مثبتة منذ زمن طويل'); - assert.equal(model.mightContainRTL(), true); + assert.strictEqual(model.mightContainRTL(), true); model.setValue('hello world!'); - assert.equal(model.mightContainRTL(), false); + assert.strictEqual(model.mightContainRTL(), false); }); }); @@ -1012,24 +1012,24 @@ suite('TextModel.createSnapshot', () => { test('empty file', () => { let model = createTextModel(''); let snapshot = model.createSnapshot(); - assert.equal(snapshot.read(), null); + assert.strictEqual(snapshot.read(), null); model.dispose(); }); test('file with BOM', () => { let model = createTextModel(UTF8_BOM_CHARACTER + 'Hello'); - assert.equal(model.getLineContent(1), 'Hello'); + assert.strictEqual(model.getLineContent(1), 'Hello'); let snapshot = model.createSnapshot(true); - assert.equal(snapshot.read(), UTF8_BOM_CHARACTER + 'Hello'); - assert.equal(snapshot.read(), null); + assert.strictEqual(snapshot.read(), UTF8_BOM_CHARACTER + 'Hello'); + assert.strictEqual(snapshot.read(), null); model.dispose(); }); test('regular file', () => { let model = createTextModel('My First Line\n\t\tMy Second Line\n Third Line\n\n1'); let snapshot = model.createSnapshot(); - assert.equal(snapshot.read(), 'My First Line\n\t\tMy Second Line\n Third Line\n\n1'); - assert.equal(snapshot.read(), null); + assert.strictEqual(snapshot.read(), 'My First Line\n\t\tMy Second Line\n Third Line\n\n1'); + assert.strictEqual(snapshot.read(), null); model.dispose(); }); @@ -1054,10 +1054,10 @@ suite('TextModel.createSnapshot', () => { // all good } else { actual += tmp2; - assert.equal(snapshot.read(), null); + assert.strictEqual(snapshot.read(), null); } - assert.equal(actual, text); + assert.strictEqual(actual, text); model.dispose(); }); diff --git a/src/vs/editor/test/common/model/textModelSearch.test.ts b/src/vs/editor/test/common/model/textModelSearch.test.ts index 715cd303..55866474 100644 --- a/src/vs/editor/test/common/model/textModelSearch.test.ts +++ b/src/vs/editor/test/common/model/textModelSearch.test.ts @@ -19,31 +19,31 @@ suite('TextModelSearch', () => { const usualWordSeparators = getMapForWordSeparators(USUAL_WORD_SEPARATORS); function assertFindMatch(actual: FindMatch | null, expectedRange: Range, expectedMatches: string[] | null = null): void { - assert.deepEqual(actual, new FindMatch(expectedRange, expectedMatches)); + assert.deepStrictEqual(actual, new FindMatch(expectedRange, expectedMatches)); } function _assertFindMatches(model: TextModel, searchParams: SearchParams, expectedMatches: FindMatch[]): void { let actual = TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), false, 1000); - assert.deepEqual(actual, expectedMatches, 'findMatches OK'); + assert.deepStrictEqual(actual, expectedMatches, 'findMatches OK'); // test `findNextMatch` let startPos = new Position(1, 1); let match = TextModelSearch.findNextMatch(model, searchParams, startPos, false); - assert.deepEqual(match, expectedMatches[0], `findNextMatch ${startPos}`); + assert.deepStrictEqual(match, expectedMatches[0], `findNextMatch ${startPos}`); for (const expectedMatch of expectedMatches) { startPos = expectedMatch.range.getStartPosition(); match = TextModelSearch.findNextMatch(model, searchParams, startPos, false); - assert.deepEqual(match, expectedMatch, `findNextMatch ${startPos}`); + assert.deepStrictEqual(match, expectedMatch, `findNextMatch ${startPos}`); } // test `findPrevMatch` startPos = new Position(model.getLineCount(), model.getLineMaxColumn(model.getLineCount())); match = TextModelSearch.findPreviousMatch(model, searchParams, startPos, false); - assert.deepEqual(match, expectedMatches[expectedMatches.length - 1], `findPrevMatch ${startPos}`); + assert.deepStrictEqual(match, expectedMatches[expectedMatches.length - 1], `findPrevMatch ${startPos}`); for (const expectedMatch of expectedMatches) { startPos = expectedMatch.range.getEndPosition(); match = TextModelSearch.findPreviousMatch(model, searchParams, startPos, false); - assert.deepEqual(match, expectedMatch, `findPrevMatch ${startPos}`); + assert.deepStrictEqual(match, expectedMatch, `findPrevMatch ${startPos}`); } } @@ -486,7 +486,7 @@ suite('TextModelSearch', () => { let searchParams = new SearchParams('(l(in)e)', true, false, null); let actual = TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), true, 100); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ new FindMatch(new Range(1, 5, 1, 9), ['line', 'line', 'in']), new FindMatch(new Range(1, 10, 1, 14), ['line', 'line', 'in']), new FindMatch(new Range(2, 5, 2, 9), ['line', 'line', 'in']), @@ -501,7 +501,7 @@ suite('TextModelSearch', () => { let searchParams = new SearchParams('(l(in)e)\\n', true, false, null); let actual = TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), true, 100); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ new FindMatch(new Range(1, 10, 2, 1), ['line\n', 'line', 'in']), new FindMatch(new Range(2, 5, 3, 1), ['line\n', 'line', 'in']), ]); @@ -556,7 +556,7 @@ suite('TextModelSearch', () => { test('\\n matches \\r\\n', () => { let model = createTextModel('a\r\nb\r\nc\r\nd\r\ne\r\nf\r\ng\r\nh\r\ni'); - assert.equal(model.getEOL(), '\r\n'); + assert.strictEqual(model.getEOL(), '\r\n'); let searchParams = new SearchParams('h\\n', true, false, null); let actual = TextModelSearch.findNextMatch(model, searchParams, new Position(1, 1), true); @@ -579,12 +579,12 @@ suite('TextModelSearch', () => { test('\\r can never be found', () => { let model = createTextModel('a\r\nb\r\nc\r\nd\r\ne\r\nf\r\ng\r\nh\r\ni'); - assert.equal(model.getEOL(), '\r\n'); + assert.strictEqual(model.getEOL(), '\r\n'); let searchParams = new SearchParams('\\r\\n', true, false, null); let actual = TextModelSearch.findNextMatch(model, searchParams, new Position(1, 1), true); - assert.equal(actual, null); - assert.deepEqual(TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), true, 1000), []); + assert.strictEqual(actual, null); + assert.deepStrictEqual(TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), true, 1000), []); model.dispose(); }); @@ -596,8 +596,8 @@ suite('TextModelSearch', () => { if (expected === null) { assert.ok(actual === null); } else { - assert.deepEqual(actual!.regex, expected.regex); - assert.deepEqual(actual!.simpleSearch, expected.simpleSearch); + assert.deepStrictEqual(actual!.regex, expected.regex); + assert.deepStrictEqual(actual!.simpleSearch, expected.simpleSearch); if (wordSeparators) { assert.ok(actual!.wordSeparators !== null); } else { @@ -769,7 +769,7 @@ suite('TextModelSearch', () => { let searchParams = new SearchParams('\\d*', true, false, null); let actual = TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), true, 100); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ new FindMatch(new Range(1, 1, 1, 3), ['10']), new FindMatch(new Range(1, 3, 1, 3), ['']), new FindMatch(new Range(1, 4, 1, 7), ['243']), diff --git a/src/vs/editor/test/common/model/textModelWithTokens.test.ts b/src/vs/editor/test/common/model/textModelWithTokens.test.ts index efb1cf1d..e4560fd8 100644 --- a/src/vs/editor/test/common/model/textModelWithTokens.test.ts +++ b/src/vs/editor/test/common/model/textModelWithTokens.test.ts @@ -100,7 +100,7 @@ suite('TextModelWithTokens', () => { column: column }); - assert.deepEqual(toRelaxedFoundBracket(actual), toRelaxedFoundBracket(currentExpectedBracket), 'findPrevBracket of ' + lineNumber + ', ' + column); + assert.deepStrictEqual(toRelaxedFoundBracket(actual), toRelaxedFoundBracket(currentExpectedBracket), 'findPrevBracket of ' + lineNumber + ', ' + column); } } } @@ -126,7 +126,7 @@ suite('TextModelWithTokens', () => { column: column }); - assert.deepEqual(toRelaxedFoundBracket(actual), toRelaxedFoundBracket(currentExpectedBracket), 'findNextBracket of ' + lineNumber + ', ' + column); + assert.deepStrictEqual(toRelaxedFoundBracket(actual), toRelaxedFoundBracket(currentExpectedBracket), 'findNextBracket of ' + lineNumber + ', ' + column); } } } @@ -148,12 +148,12 @@ suite('TextModelWithTokens', () => { function assertIsNotBracket(model: TextModel, lineNumber: number, column: number) { const match = model.matchBracket(new Position(lineNumber, column)); - assert.equal(match, null, 'is not matching brackets at ' + lineNumber + ', ' + column); + assert.strictEqual(match, null, 'is not matching brackets at ' + lineNumber + ', ' + column); } function assertIsBracket(model: TextModel, testPosition: Position, expected: [Range, Range]): void { const actual = model.matchBracket(testPosition); - assert.deepEqual(actual, expected, 'matches brackets at ' + testPosition); + assert.deepStrictEqual(actual, expected, 'matches brackets at ' + testPosition); } suite('TextModelWithTokens - bracket matching', () => { @@ -351,7 +351,7 @@ suite('TextModelWithTokens', () => { const tokenizationSupport: ITokenizationSupport = { getInitialState: () => NULL_STATE, tokenize: undefined!, - tokenize2: (line, state) => { + tokenize2: (line, hasEOL, state) => { switch (line) { case 'function hello() {': { const tokens = new Uint32Array([ @@ -399,8 +399,8 @@ suite('TextModelWithTokens', () => { model.forceTokenization(2); model.forceTokenization(3); - assert.deepEqual(model.matchBracket(new Position(2, 23)), null); - assert.deepEqual(model.matchBracket(new Position(2, 20)), null); + assert.deepStrictEqual(model.matchBracket(new Position(2, 23)), null); + assert.deepStrictEqual(model.matchBracket(new Position(2, 20)), null); model.dispose(); registration1.dispose(); @@ -434,7 +434,7 @@ suite('TextModelWithTokens regression tests', () => { foreground: token.getForeground() }; }; - assert.deepEqual(actual, expected.map(decode)); + assert.deepStrictEqual(actual, expected.map(decode)); } let _tokenId = 10; @@ -446,7 +446,7 @@ suite('TextModelWithTokens regression tests', () => { const tokenizationSupport: ITokenizationSupport = { getInitialState: () => NULL_STATE, tokenize: undefined!, - tokenize2: (line, state) => { + tokenize2: (line, hasEOL, state) => { let myId = ++_tokenId; let tokens = new Uint32Array(2); tokens[0] = 0; @@ -512,7 +512,7 @@ suite('TextModelWithTokens regression tests', () => { ].join('\n'), undefined, languageIdentifier); let actual = model.matchBracket(new Position(4, 1)); - assert.deepEqual(actual, [new Range(4, 1, 4, 7), new Range(9, 1, 9, 11)]); + assert.deepStrictEqual(actual, [new Range(4, 1, 4, 7), new Range(9, 1, 9, 11)]); model.dispose(); registration.dispose(); @@ -537,7 +537,7 @@ suite('TextModelWithTokens regression tests', () => { ].join('\n'), undefined, languageIdentifier); let actual = model.matchBracket(new Position(3, 9)); - assert.deepEqual(actual, [new Range(3, 6, 3, 17), new Range(2, 6, 2, 14)]); + assert.deepStrictEqual(actual, [new Range(3, 6, 3, 17), new Range(2, 6, 2, 14)]); model.dispose(); registration.dispose(); @@ -550,7 +550,7 @@ suite('TextModelWithTokens regression tests', () => { const tokenizationSupport: ITokenizationSupport = { getInitialState: () => NULL_STATE, tokenize: undefined!, - tokenize2: (line, state) => { + tokenize2: (line, hasEOL, state) => { let tokens = new Uint32Array(2); tokens[0] = 0; tokens[1] = ( @@ -565,7 +565,7 @@ suite('TextModelWithTokens regression tests', () => { let model = createTextModel('A model with one line', undefined, outerMode); model.forceTokenization(1); - assert.equal(model.getLanguageIdAtPosition(1, 1), innerMode.id); + assert.strictEqual(model.getLanguageIdAtPosition(1, 1), innerMode.id); model.dispose(); registration.dispose(); @@ -586,7 +586,7 @@ suite('TextModel.getLineIndentGuide', () => { actual[line - 1] = [actualIndents[line - 1], activeIndentGuide.startLineNumber, activeIndentGuide.endLineNumber, activeIndentGuide.indent, model.getLineContent(line)]; } - assert.deepEqual(actual, lines); + assert.deepStrictEqual(actual, lines); model.dispose(); } @@ -764,7 +764,7 @@ suite('TextModel.getLineIndentGuide', () => { ].join('\n')); const actual = model.getActiveIndentGuide(2, 4, 9); - assert.deepEqual(actual, { startLineNumber: 2, endLineNumber: 9, indent: 1 }); + assert.deepStrictEqual(actual, { startLineNumber: 2, endLineNumber: 9, indent: 1 }); model.dispose(); }); diff --git a/src/vs/editor/test/common/model/tokensStore.test.ts b/src/vs/editor/test/common/model/tokensStore.test.ts index ee8b438b..e883d551 100644 --- a/src/vs/editor/test/common/model/tokensStore.test.ts +++ b/src/vs/editor/test/common/model/tokensStore.test.ts @@ -104,7 +104,7 @@ suite('TokensStore', () => { model.applyEdits(edits); const actualState = extractState(model); - assert.deepEqual(actualState, rawFinalState); + assert.deepStrictEqual(actualState, rawFinalState); model.dispose(); } @@ -191,7 +191,7 @@ suite('TokensStore', () => { decodedTokens.push(lineTokens.getEndOffset(i), lineTokens.getMetadata(i)); } - assert.deepEqual(decodedTokens, [ + assert.deepStrictEqual(decodedTokens, [ 20, 16793600, 24, 17022976, 25, 16793600, @@ -252,7 +252,7 @@ suite('TokensStore', () => { ]); const lineTokens = store.addSemanticTokens(10, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`)); - assert.equal(lineTokens.getCount(), 3); + assert.strictEqual(lineTokens.getCount(), 3); }); test('partial tokens 2', () => { @@ -293,7 +293,7 @@ suite('TokensStore', () => { ]); const lineTokens = store.addSemanticTokens(20, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`)); - assert.equal(lineTokens.getCount(), 3); + assert.strictEqual(lineTokens.getCount(), 3); }); test('partial tokens 3', () => { @@ -320,7 +320,7 @@ suite('TokensStore', () => { ]); const lineTokens = store.addSemanticTokens(5, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`)); - assert.equal(lineTokens.getCount(), 3); + assert.strictEqual(lineTokens.getCount(), 3); }); test('issue #94133: Semantic colors stick around when using (only) range provider', () => { @@ -337,7 +337,7 @@ suite('TokensStore', () => { store.setPartial(new Range(1, 1, 1, 20), []); const lineTokens = store.addSemanticTokens(1, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`)); - assert.equal(lineTokens.getCount(), 1); + assert.strictEqual(lineTokens.getCount(), 1); }); test('bug', () => { @@ -385,7 +385,7 @@ suite('TokensStore', () => { ); const lineTokens = store.addSemanticTokens(36451, new LineTokens(new Uint32Array([60, 1]), ` if (flags & ModifierFlags.Ambient) {`)); - assert.equal(lineTokens.getCount(), 7); + assert.strictEqual(lineTokens.getCount(), 7); }); @@ -424,7 +424,7 @@ suite('TokensStore', () => { ]), `const hello = 123;`)); const actual = toArr(lineTokens); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ 5, createTMMetadata(5, FontStyle.Bold, 53), 6, createTMMetadata(1, FontStyle.None, 53), 11, createTMMetadata(1, FontStyle.None, 53), diff --git a/src/vs/editor/test/common/modes/languageConfiguration.test.ts b/src/vs/editor/test/common/modes/languageConfiguration.test.ts index e898568a..3acba6dd 100644 --- a/src/vs/editor/test/common/modes/languageConfiguration.test.ts +++ b/src/vs/editor/test/common/modes/languageConfiguration.test.ts @@ -11,81 +11,81 @@ suite('StandardAutoClosingPairConditional', () => { test('Missing notIn', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}' }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), true); - assert.equal(v.isOK(StandardTokenType.String), true); - assert.equal(v.isOK(StandardTokenType.RegEx), true); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), true); + assert.strictEqual(v.isOK(StandardTokenType.String), true); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), true); }); test('Empty notIn', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: [] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), true); - assert.equal(v.isOK(StandardTokenType.String), true); - assert.equal(v.isOK(StandardTokenType.RegEx), true); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), true); + assert.strictEqual(v.isOK(StandardTokenType.String), true); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), true); }); test('Invalid notIn', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['bla'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), true); - assert.equal(v.isOK(StandardTokenType.String), true); - assert.equal(v.isOK(StandardTokenType.RegEx), true); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), true); + assert.strictEqual(v.isOK(StandardTokenType.String), true); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), true); }); test('notIn in strings', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['string'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), true); - assert.equal(v.isOK(StandardTokenType.String), false); - assert.equal(v.isOK(StandardTokenType.RegEx), true); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), true); + assert.strictEqual(v.isOK(StandardTokenType.String), false); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), true); }); test('notIn in comments', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['comment'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), false); - assert.equal(v.isOK(StandardTokenType.String), true); - assert.equal(v.isOK(StandardTokenType.RegEx), true); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), false); + assert.strictEqual(v.isOK(StandardTokenType.String), true); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), true); }); test('notIn in regex', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['regex'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), true); - assert.equal(v.isOK(StandardTokenType.String), true); - assert.equal(v.isOK(StandardTokenType.RegEx), false); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), true); + assert.strictEqual(v.isOK(StandardTokenType.String), true); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), false); }); test('notIn in strings nor comments', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['string', 'comment'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), false); - assert.equal(v.isOK(StandardTokenType.String), false); - assert.equal(v.isOK(StandardTokenType.RegEx), true); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), false); + assert.strictEqual(v.isOK(StandardTokenType.String), false); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), true); }); test('notIn in strings nor regex', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['string', 'regex'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), true); - assert.equal(v.isOK(StandardTokenType.String), false); - assert.equal(v.isOK(StandardTokenType.RegEx), false); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), true); + assert.strictEqual(v.isOK(StandardTokenType.String), false); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), false); }); test('notIn in comments nor regex', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['comment', 'regex'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), false); - assert.equal(v.isOK(StandardTokenType.String), true); - assert.equal(v.isOK(StandardTokenType.RegEx), false); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), false); + assert.strictEqual(v.isOK(StandardTokenType.String), true); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), false); }); test('notIn in strings, comments nor regex', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['string', 'comment', 'regex'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), false); - assert.equal(v.isOK(StandardTokenType.String), false); - assert.equal(v.isOK(StandardTokenType.RegEx), false); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), false); + assert.strictEqual(v.isOK(StandardTokenType.String), false); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), false); }); }); diff --git a/src/vs/editor/test/common/modes/languageSelector.test.ts b/src/vs/editor/test/common/modes/languageSelector.test.ts index 0886c65b..dc06141f 100644 --- a/src/vs/editor/test/common/modes/languageSelector.test.ts +++ b/src/vs/editor/test/common/modes/languageSelector.test.ts @@ -15,18 +15,18 @@ suite('LanguageSelector', function () { }; test('score, invalid selector', function () { - assert.equal(score({}, model.uri, model.language, true), 0); - assert.equal(score(undefined!, model.uri, model.language, true), 0); - assert.equal(score(null!, model.uri, model.language, true), 0); - assert.equal(score('', model.uri, model.language, true), 0); + assert.strictEqual(score({}, model.uri, model.language, true), 0); + assert.strictEqual(score(undefined!, model.uri, model.language, true), 0); + assert.strictEqual(score(null!, model.uri, model.language, true), 0); + assert.strictEqual(score('', model.uri, model.language, true), 0); }); test('score, any language', function () { - assert.equal(score({ language: '*' }, model.uri, model.language, true), 5); - assert.equal(score('*', model.uri, model.language, true), 5); + assert.strictEqual(score({ language: '*' }, model.uri, model.language, true), 5); + assert.strictEqual(score('*', model.uri, model.language, true), 5); - assert.equal(score('*', URI.parse('foo:bar'), model.language, true), 5); - assert.equal(score('farboo', URI.parse('foo:bar'), model.language, true), 10); + assert.strictEqual(score('*', URI.parse('foo:bar'), model.language, true), 5); + assert.strictEqual(score('farboo', URI.parse('foo:bar'), model.language, true), 10); }); test('score, default schemes', function () { @@ -34,50 +34,50 @@ suite('LanguageSelector', function () { const uri = URI.parse('git:foo/file.txt'); const language = 'farboo'; - assert.equal(score('*', uri, language, true), 5); - assert.equal(score('farboo', uri, language, true), 10); - assert.equal(score({ language: 'farboo', scheme: '' }, uri, language, true), 10); - assert.equal(score({ language: 'farboo', scheme: 'git' }, uri, language, true), 10); - assert.equal(score({ language: 'farboo', scheme: '*' }, uri, language, true), 10); - assert.equal(score({ language: 'farboo' }, uri, language, true), 10); - assert.equal(score({ language: '*' }, uri, language, true), 5); + assert.strictEqual(score('*', uri, language, true), 5); + assert.strictEqual(score('farboo', uri, language, true), 10); + assert.strictEqual(score({ language: 'farboo', scheme: '' }, uri, language, true), 10); + assert.strictEqual(score({ language: 'farboo', scheme: 'git' }, uri, language, true), 10); + assert.strictEqual(score({ language: 'farboo', scheme: '*' }, uri, language, true), 10); + assert.strictEqual(score({ language: 'farboo' }, uri, language, true), 10); + assert.strictEqual(score({ language: '*' }, uri, language, true), 5); - assert.equal(score({ scheme: '*' }, uri, language, true), 5); - assert.equal(score({ scheme: 'git' }, uri, language, true), 10); + assert.strictEqual(score({ scheme: '*' }, uri, language, true), 5); + assert.strictEqual(score({ scheme: 'git' }, uri, language, true), 10); }); test('score, filter', function () { - assert.equal(score('farboo', model.uri, model.language, true), 10); - assert.equal(score({ language: 'farboo' }, model.uri, model.language, true), 10); - assert.equal(score({ language: 'farboo', scheme: 'file' }, model.uri, model.language, true), 10); - assert.equal(score({ language: 'farboo', scheme: 'http' }, model.uri, model.language, true), 0); + assert.strictEqual(score('farboo', model.uri, model.language, true), 10); + assert.strictEqual(score({ language: 'farboo' }, model.uri, model.language, true), 10); + assert.strictEqual(score({ language: 'farboo', scheme: 'file' }, model.uri, model.language, true), 10); + assert.strictEqual(score({ language: 'farboo', scheme: 'http' }, model.uri, model.language, true), 0); - assert.equal(score({ pattern: '**/*.fb' }, model.uri, model.language, true), 10); - assert.equal(score({ pattern: '**/*.fb', scheme: 'file' }, model.uri, model.language, true), 10); - assert.equal(score({ pattern: '**/*.fb' }, URI.parse('foo:bar'), model.language, true), 0); - assert.equal(score({ pattern: '**/*.fb', scheme: 'foo' }, URI.parse('foo:bar'), model.language, true), 0); + assert.strictEqual(score({ pattern: '**/*.fb' }, model.uri, model.language, true), 10); + assert.strictEqual(score({ pattern: '**/*.fb', scheme: 'file' }, model.uri, model.language, true), 10); + assert.strictEqual(score({ pattern: '**/*.fb' }, URI.parse('foo:bar'), model.language, true), 0); + assert.strictEqual(score({ pattern: '**/*.fb', scheme: 'foo' }, URI.parse('foo:bar'), model.language, true), 0); let doc = { uri: URI.parse('git:/my/file.js'), langId: 'javascript' }; - assert.equal(score('javascript', doc.uri, doc.langId, true), 10); // 0; - assert.equal(score({ language: 'javascript', scheme: 'git' }, doc.uri, doc.langId, true), 10); // 10; - assert.equal(score('*', doc.uri, doc.langId, true), 5); // 5 - assert.equal(score('fooLang', doc.uri, doc.langId, true), 0); // 0 - assert.equal(score(['fooLang', '*'], doc.uri, doc.langId, true), 5); // 5 + assert.strictEqual(score('javascript', doc.uri, doc.langId, true), 10); // 0; + assert.strictEqual(score({ language: 'javascript', scheme: 'git' }, doc.uri, doc.langId, true), 10); // 10; + assert.strictEqual(score('*', doc.uri, doc.langId, true), 5); // 5 + assert.strictEqual(score('fooLang', doc.uri, doc.langId, true), 0); // 0 + assert.strictEqual(score(['fooLang', '*'], doc.uri, doc.langId, true), 5); // 5 }); test('score, max(filters)', function () { let match = { language: 'farboo', scheme: 'file' }; let fail = { language: 'farboo', scheme: 'http' }; - assert.equal(score(match, model.uri, model.language, true), 10); - assert.equal(score(fail, model.uri, model.language, true), 0); - assert.equal(score([match, fail], model.uri, model.language, true), 10); - assert.equal(score([fail, fail], model.uri, model.language, true), 0); - assert.equal(score(['farboo', '*'], model.uri, model.language, true), 10); - assert.equal(score(['*', 'farboo'], model.uri, model.language, true), 10); + assert.strictEqual(score(match, model.uri, model.language, true), 10); + assert.strictEqual(score(fail, model.uri, model.language, true), 0); + assert.strictEqual(score([match, fail], model.uri, model.language, true), 10); + assert.strictEqual(score([fail, fail], model.uri, model.language, true), 0); + assert.strictEqual(score(['farboo', '*'], model.uri, model.language, true), 10); + assert.strictEqual(score(['*', 'farboo'], model.uri, model.language, true), 10); }); test('score hasAccessToAllModels', function () { @@ -85,14 +85,14 @@ suite('LanguageSelector', function () { uri: URI.parse('file:/my/file.js'), langId: 'javascript' }; - assert.equal(score('javascript', doc.uri, doc.langId, false), 0); - assert.equal(score({ language: 'javascript', scheme: 'file' }, doc.uri, doc.langId, false), 0); - assert.equal(score('*', doc.uri, doc.langId, false), 0); - assert.equal(score('fooLang', doc.uri, doc.langId, false), 0); - assert.equal(score(['fooLang', '*'], doc.uri, doc.langId, false), 0); + assert.strictEqual(score('javascript', doc.uri, doc.langId, false), 0); + assert.strictEqual(score({ language: 'javascript', scheme: 'file' }, doc.uri, doc.langId, false), 0); + assert.strictEqual(score('*', doc.uri, doc.langId, false), 0); + assert.strictEqual(score('fooLang', doc.uri, doc.langId, false), 0); + assert.strictEqual(score(['fooLang', '*'], doc.uri, doc.langId, false), 0); - assert.equal(score({ language: 'javascript', scheme: 'file', hasAccessToAllModels: true }, doc.uri, doc.langId, false), 10); - assert.equal(score(['fooLang', '*', { language: '*', hasAccessToAllModels: true }], doc.uri, doc.langId, false), 5); + assert.strictEqual(score({ language: 'javascript', scheme: 'file', hasAccessToAllModels: true }, doc.uri, doc.langId, false), 10); + assert.strictEqual(score(['fooLang', '*', { language: '*', hasAccessToAllModels: true }], doc.uri, doc.langId, false), 5); }); test('Document selector match - unexpected result value #60232', function () { @@ -102,7 +102,7 @@ suite('LanguageSelector', function () { pattern: '**/*.interface.json' }; let value = score(selector, URI.parse('file:///C:/Users/zlhe/Desktop/test.interface.json'), 'json', true); - assert.equal(value, 10); + assert.strictEqual(value, 10); }); test('Document selector match - platform paths #99938', function () { @@ -113,6 +113,6 @@ suite('LanguageSelector', function () { } }; let value = score(selector, URI.file('/home/user/Desktop/test.json'), 'json', true); - assert.equal(value, 10); + assert.strictEqual(value, 10); }); }); diff --git a/src/vs/editor/test/common/modes/linkComputer.test.ts b/src/vs/editor/test/common/modes/linkComputer.test.ts index 9cf9ca77..5bb34c91 100644 --- a/src/vs/editor/test/common/modes/linkComputer.test.ts +++ b/src/vs/editor/test/common/modes/linkComputer.test.ts @@ -49,7 +49,7 @@ function assertLink(text: string, extractedLink: string): void { } let r = myComputeLinks([text]); - assert.deepEqual(r, [{ + assert.deepStrictEqual(r, [{ range: { startLineNumber: 1, startColumn: startColumn, @@ -64,7 +64,7 @@ suite('Editor Modes - Link Computer', () => { test('Null model', () => { let r = computeLinks(null); - assert.deepEqual(r, []); + assert.deepStrictEqual(r, []); }); test('Parsing', () => { diff --git a/src/vs/editor/test/common/modes/supports/characterPair.test.ts b/src/vs/editor/test/common/modes/supports/characterPair.test.ts index e244d6b4..ce1c31ee 100644 --- a/src/vs/editor/test/common/modes/supports/characterPair.test.ts +++ b/src/vs/editor/test/common/modes/supports/characterPair.test.ts @@ -13,44 +13,44 @@ suite('CharacterPairSupport', () => { test('only autoClosingPairs', () => { let characaterPairSupport = new CharacterPairSupport({ autoClosingPairs: [{ open: 'a', close: 'b' }] }); - assert.deepEqual(characaterPairSupport.getAutoClosingPairs(), [{ open: 'a', close: 'b', _standardTokenMask: 0 }]); - assert.deepEqual(characaterPairSupport.getSurroundingPairs(), [{ open: 'a', close: 'b', _standardTokenMask: 0 }]); + assert.deepStrictEqual(characaterPairSupport.getAutoClosingPairs(), [new StandardAutoClosingPairConditional({ open: 'a', close: 'b' })]); + assert.deepStrictEqual(characaterPairSupport.getSurroundingPairs(), [new StandardAutoClosingPairConditional({ open: 'a', close: 'b' })]); }); test('only empty autoClosingPairs', () => { let characaterPairSupport = new CharacterPairSupport({ autoClosingPairs: [] }); - assert.deepEqual(characaterPairSupport.getAutoClosingPairs(), []); - assert.deepEqual(characaterPairSupport.getSurroundingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getAutoClosingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getSurroundingPairs(), []); }); test('only brackets', () => { let characaterPairSupport = new CharacterPairSupport({ brackets: [['a', 'b']] }); - assert.deepEqual(characaterPairSupport.getAutoClosingPairs(), [{ open: 'a', close: 'b', _standardTokenMask: 0 }]); - assert.deepEqual(characaterPairSupport.getSurroundingPairs(), [{ open: 'a', close: 'b', _standardTokenMask: 0 }]); + assert.deepStrictEqual(characaterPairSupport.getAutoClosingPairs(), [new StandardAutoClosingPairConditional({ open: 'a', close: 'b' })]); + assert.deepStrictEqual(characaterPairSupport.getSurroundingPairs(), [new StandardAutoClosingPairConditional({ open: 'a', close: 'b' })]); }); test('only empty brackets', () => { let characaterPairSupport = new CharacterPairSupport({ brackets: [] }); - assert.deepEqual(characaterPairSupport.getAutoClosingPairs(), []); - assert.deepEqual(characaterPairSupport.getSurroundingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getAutoClosingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getSurroundingPairs(), []); }); test('only surroundingPairs', () => { let characaterPairSupport = new CharacterPairSupport({ surroundingPairs: [{ open: 'a', close: 'b' }] }); - assert.deepEqual(characaterPairSupport.getAutoClosingPairs(), []); - assert.deepEqual(characaterPairSupport.getSurroundingPairs(), [{ open: 'a', close: 'b' }]); + assert.deepStrictEqual(characaterPairSupport.getAutoClosingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getSurroundingPairs(), [{ open: 'a', close: 'b' }]); }); test('only empty surroundingPairs', () => { let characaterPairSupport = new CharacterPairSupport({ surroundingPairs: [] }); - assert.deepEqual(characaterPairSupport.getAutoClosingPairs(), []); - assert.deepEqual(characaterPairSupport.getSurroundingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getAutoClosingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getSurroundingPairs(), []); }); test('brackets is ignored when having autoClosingPairs', () => { let characaterPairSupport = new CharacterPairSupport({ autoClosingPairs: [], brackets: [['a', 'b']] }); - assert.deepEqual(characaterPairSupport.getAutoClosingPairs(), []); - assert.deepEqual(characaterPairSupport.getSurroundingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getAutoClosingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getSurroundingPairs(), []); }); function findAutoClosingPair(characterPairSupport: CharacterPairSupport, character: string): StandardAutoClosingPairConditional | undefined { @@ -67,64 +67,64 @@ suite('CharacterPairSupport', () => { test('shouldAutoClosePair in empty line', () => { let sup = new CharacterPairSupport({ autoClosingPairs: [{ open: '{', close: '}', notIn: ['string', 'comment'] }] }); - assert.equal(testShouldAutoClose(sup, [], 'a', 1), false); - assert.equal(testShouldAutoClose(sup, [], '{', 1), true); + assert.strictEqual(testShouldAutoClose(sup, [], 'a', 1), false); + assert.strictEqual(testShouldAutoClose(sup, [], '{', 1), true); }); test('shouldAutoClosePair in not interesting line 1', () => { let sup = new CharacterPairSupport({ autoClosingPairs: [{ open: '{', close: '}', notIn: ['string', 'comment'] }] }); - assert.equal(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.Other }], '{', 3), true); - assert.equal(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.Other }], 'a', 3), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.Other }], '{', 3), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.Other }], 'a', 3), false); }); test('shouldAutoClosePair in not interesting line 2', () => { let sup = new CharacterPairSupport({ autoClosingPairs: [{ open: '{', close: '}' }] }); - assert.equal(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.String }], '{', 3), true); - assert.equal(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.String }], 'a', 3), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.String }], '{', 3), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.String }], 'a', 3), false); }); test('shouldAutoClosePair in interesting line 1', () => { let sup = new CharacterPairSupport({ autoClosingPairs: [{ open: '{', close: '}', notIn: ['string', 'comment'] }] }); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 1), false); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 1), false); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 2), false); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 2), false); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 3), false); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 3), false); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 4), false); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 4), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 1), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 1), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 2), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 2), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 3), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 3), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 4), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 4), false); }); test('shouldAutoClosePair in interesting line 2', () => { let sup = new CharacterPairSupport({ autoClosingPairs: [{ open: '{', close: '}', notIn: ['string', 'comment'] }] }); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 1), true); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 1), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 2), true); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 2), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 3), true); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 3), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 4), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 4), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 5), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 5), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 6), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 6), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 7), true); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 7), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 1), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 1), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 2), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 2), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 3), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 3), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 4), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 4), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 5), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 5), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 6), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 6), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 7), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 7), false); }); test('shouldAutoClosePair in interesting line 3', () => { let sup = new CharacterPairSupport({ autoClosingPairs: [{ open: '{', close: '}', notIn: ['string', 'comment'] }] }); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 1), true); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 1), false); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 2), true); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 2), false); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 3), false); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 3), false); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 4), false); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 4), false); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 5), false); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 5), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 1), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 1), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 2), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 2), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 3), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 3), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 4), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 4), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 5), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 5), false); }); }); diff --git a/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts b/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts index 22b818c6..ba609bdb 100644 --- a/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts +++ b/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts @@ -18,12 +18,12 @@ suite('Editor Modes - Auto Indentation', () => { function testDoesNothing(electricCharacterSupport: BracketElectricCharacterSupport, line: TokenText[], character: string, offset: number): void { let actual = _testOnElectricCharacter(electricCharacterSupport, line, character, offset); - assert.deepEqual(actual, null); + assert.deepStrictEqual(actual, null); } function testMatchBracket(electricCharacterSupport: BracketElectricCharacterSupport, line: TokenText[], character: string, offset: number, matchOpenBracket: string): void { let actual = _testOnElectricCharacter(electricCharacterSupport, line, character, offset); - assert.deepEqual(actual, { matchOpenBracket: matchOpenBracket }); + assert.deepStrictEqual(actual, { matchOpenBracket: matchOpenBracket }); } test('getElectricCharacters uses all sources and dedups', () => { @@ -34,7 +34,7 @@ suite('Editor Modes - Auto Indentation', () => { ]) ); - assert.deepEqual(sup.getElectricCharacters(), ['}', ')']); + assert.deepStrictEqual(sup.getElectricCharacters(), ['}', ')']); }); test('matchOpenBracket', () => { diff --git a/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts b/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts index 72220480..83d8a6ce 100644 --- a/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts +++ b/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts @@ -18,7 +18,7 @@ export const javascriptOnEnterRules = [ }, { // e.g. * ...| beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/, - oneLineAboveText: /(?=^(\s*(\/\*\*|\*)).*)(?=(?!(\s*\*\/)))/, + previousLineText: /(?=^(\s*(\/\*\*|\*)).*)(?=(?!(\s*\*\/)))/, action: { indentAction: IndentAction.None, appendText: '* ' } }, { // e.g. */| diff --git a/src/vs/editor/test/common/modes/supports/onEnter.test.ts b/src/vs/editor/test/common/modes/supports/onEnter.test.ts index f799c66e..1af36e9e 100644 --- a/src/vs/editor/test/common/modes/supports/onEnter.test.ts +++ b/src/vs/editor/test/common/modes/supports/onEnter.test.ts @@ -21,9 +21,9 @@ suite('OnEnter', () => { let testIndentAction = (beforeText: string, afterText: string, expected: IndentAction) => { let actual = support.onEnter(EditorAutoIndentStrategy.Advanced, '', beforeText, afterText); if (expected === IndentAction.None) { - assert.equal(actual, null); + assert.strictEqual(actual, null); } else { - assert.equal(actual!.indentAction, expected); + assert.strictEqual(actual!.indentAction, expected); } }; @@ -51,18 +51,18 @@ suite('OnEnter', () => { let support = new OnEnterSupport({ onEnterRules: javascriptOnEnterRules }); - let testIndentAction = (oneLineAboveText: string, beforeText: string, afterText: string, expectedIndentAction: IndentAction | null, expectedAppendText: string | null, removeText: number = 0) => { - let actual = support.onEnter(EditorAutoIndentStrategy.Advanced, oneLineAboveText, beforeText, afterText); + let testIndentAction = (previousLineText: string, beforeText: string, afterText: string, expectedIndentAction: IndentAction | null, expectedAppendText: string | null, removeText: number = 0) => { + let actual = support.onEnter(EditorAutoIndentStrategy.Advanced, previousLineText, beforeText, afterText); if (expectedIndentAction === null) { - assert.equal(actual, null, 'isNull:' + beforeText); + assert.strictEqual(actual, null, 'isNull:' + beforeText); } else { - assert.equal(actual !== null, true, 'isNotNull:' + beforeText); - assert.equal(actual!.indentAction, expectedIndentAction, 'indentAction:' + beforeText); + assert.strictEqual(actual !== null, true, 'isNotNull:' + beforeText); + assert.strictEqual(actual!.indentAction, expectedIndentAction, 'indentAction:' + beforeText); if (expectedAppendText !== null) { - assert.equal(actual!.appendText, expectedAppendText, 'appendText:' + beforeText); + assert.strictEqual(actual!.appendText, expectedAppendText, 'appendText:' + beforeText); } if (removeText !== 0) { - assert.equal(actual!.removeText, removeText, 'removeText:' + beforeText); + assert.strictEqual(actual!.removeText, removeText, 'removeText:' + beforeText); } } }; diff --git a/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts b/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts index 40ae7e62..cf6bb999 100644 --- a/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts +++ b/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts @@ -19,61 +19,61 @@ suite('richEditBrackets', () => { test('findPrevBracketInToken one char 1', () => { let result = findPrevBracketInRange(/(\{)|(\})/i, '{', 0, 1); - assert.equal(result!.startColumn, 1); - assert.equal(result!.endColumn, 2); + assert.strictEqual(result!.startColumn, 1); + assert.strictEqual(result!.endColumn, 2); }); test('findPrevBracketInToken one char 2', () => { let result = findPrevBracketInRange(/(\{)|(\})/i, '{{', 0, 1); - assert.equal(result!.startColumn, 1); - assert.equal(result!.endColumn, 2); + assert.strictEqual(result!.startColumn, 1); + assert.strictEqual(result!.endColumn, 2); }); test('findPrevBracketInToken one char 3', () => { let result = findPrevBracketInRange(/(\{)|(\})/i, '{hello world!', 0, 13); - assert.equal(result!.startColumn, 1); - assert.equal(result!.endColumn, 2); + assert.strictEqual(result!.startColumn, 1); + assert.strictEqual(result!.endColumn, 2); }); test('findPrevBracketInToken more chars 1', () => { let result = findPrevBracketInRange(/(olleh)/i, 'hello world!', 0, 12); - assert.equal(result!.startColumn, 1); - assert.equal(result!.endColumn, 6); + assert.strictEqual(result!.startColumn, 1); + assert.strictEqual(result!.endColumn, 6); }); test('findPrevBracketInToken more chars 2', () => { let result = findPrevBracketInRange(/(olleh)/i, 'hello world!', 0, 5); - assert.equal(result!.startColumn, 1); - assert.equal(result!.endColumn, 6); + assert.strictEqual(result!.startColumn, 1); + assert.strictEqual(result!.endColumn, 6); }); test('findPrevBracketInToken more chars 3', () => { let result = findPrevBracketInRange(/(olleh)/i, ' hello world!', 0, 6); - assert.equal(result!.startColumn, 2); - assert.equal(result!.endColumn, 7); + assert.strictEqual(result!.startColumn, 2); + assert.strictEqual(result!.endColumn, 7); }); test('findNextBracketInToken one char', () => { let result = findNextBracketInRange(/(\{)|(\})/i, '{', 0, 1); - assert.equal(result!.startColumn, 1); - assert.equal(result!.endColumn, 2); + assert.strictEqual(result!.startColumn, 1); + assert.strictEqual(result!.endColumn, 2); }); test('findNextBracketInToken more chars', () => { let result = findNextBracketInRange(/(world)/i, 'hello world!', 0, 12); - assert.equal(result!.startColumn, 7); - assert.equal(result!.endColumn, 12); + assert.strictEqual(result!.startColumn, 7); + assert.strictEqual(result!.endColumn, 12); }); test('findNextBracketInToken with emoty result', () => { let result = findNextBracketInRange(/(\{)|(\})/i, '', 0, 0); - assert.equal(result, null); + assert.strictEqual(result, null); }); test('issue #3894: [Handlebars] Curly braces edit issues', () => { let result = findPrevBracketInRange(/(\-\-!<)|(>\-\-)|(\{\{)|(\}\})/i, '{{asd}}', 0, 2); - assert.equal(result!.startColumn, 1); - assert.equal(result!.endColumn, 3); + assert.strictEqual(result!.startColumn, 1); + assert.strictEqual(result!.endColumn, 3); }); }); diff --git a/src/vs/editor/test/common/modes/supports/tokenization.test.ts b/src/vs/editor/test/common/modes/supports/tokenization.test.ts index 9f2bd1b9..a8f90408 100644 --- a/src/vs/editor/test/common/modes/supports/tokenization.test.ts +++ b/src/vs/editor/test/common/modes/supports/tokenization.test.ts @@ -24,7 +24,7 @@ suite('Token theme matching', () => { let actual = theme._match('punctuation.definition.string.begin.html'); - assert.deepEqual(actual, new ThemeTrieElementRule(FontStyle.None, _D, _B)); + assert.deepStrictEqual(actual, new ThemeTrieElementRule(FontStyle.None, _D, _B)); }); test('can match', () => { @@ -55,7 +55,7 @@ suite('Token theme matching', () => { function assertMatch(scopeName: string, expected: ThemeTrieElementRule): void { let actual = theme._match(scopeName); - assert.deepEqual(actual, expected, 'when matching <<' + scopeName + '>>'); + assert.deepStrictEqual(actual, expected, 'when matching <<' + scopeName + '>>'); } function assertSimpleMatch(scopeName: string, fontStyle: FontStyle, foreground: number, background: number): void { @@ -152,7 +152,7 @@ suite('Token theme parsing', () => { new ParsedTokenThemeRule('constant.numeric.dec', 10, FontStyle.None, '0000ff', null), ]; - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); }); @@ -162,7 +162,7 @@ suite('Token theme resolving', () => { let actual = ['bar', 'z', 'zu', 'a', 'ab', ''].sort(strcmp); let expected = ['', 'a', 'ab', 'bar', 'z', 'zu']; - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); test('always has defaults', () => { @@ -170,8 +170,8 @@ suite('Token theme resolving', () => { let colorMap = new ColorMap(); const _A = colorMap.getId('000000'); const _B = colorMap.getId('ffffff'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); - assert.deepEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); }); test('respects incoming defaults 1', () => { @@ -181,8 +181,8 @@ suite('Token theme resolving', () => { let colorMap = new ColorMap(); const _A = colorMap.getId('000000'); const _B = colorMap.getId('ffffff'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); - assert.deepEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); }); test('respects incoming defaults 2', () => { @@ -192,8 +192,8 @@ suite('Token theme resolving', () => { let colorMap = new ColorMap(); const _A = colorMap.getId('000000'); const _B = colorMap.getId('ffffff'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); - assert.deepEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); }); test('respects incoming defaults 3', () => { @@ -203,8 +203,8 @@ suite('Token theme resolving', () => { let colorMap = new ColorMap(); const _A = colorMap.getId('000000'); const _B = colorMap.getId('ffffff'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); - assert.deepEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _A, _B))); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _A, _B))); }); test('respects incoming defaults 4', () => { @@ -214,8 +214,8 @@ suite('Token theme resolving', () => { let colorMap = new ColorMap(); const _A = colorMap.getId('ff0000'); const _B = colorMap.getId('ffffff'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); - assert.deepEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); }); test('respects incoming defaults 5', () => { @@ -225,8 +225,8 @@ suite('Token theme resolving', () => { let colorMap = new ColorMap(); const _A = colorMap.getId('000000'); const _B = colorMap.getId('ff0000'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); - assert.deepEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); }); test('can merge incoming defaults', () => { @@ -238,8 +238,8 @@ suite('Token theme resolving', () => { let colorMap = new ColorMap(); const _A = colorMap.getId('00ff00'); const _B = colorMap.getId('ff0000'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); - assert.deepEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _A, _B))); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _A, _B))); }); test('defaults are inherited', () => { @@ -251,7 +251,7 @@ suite('Token theme resolving', () => { const _A = colorMap.getId('F8F8F2'); const _B = colorMap.getId('272822'); const _C = colorMap.getId('ff0000'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); let root = new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B), { 'var': new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _C, _B)) }); @@ -268,7 +268,7 @@ suite('Token theme resolving', () => { const _A = colorMap.getId('F8F8F2'); const _B = colorMap.getId('272822'); const _C = colorMap.getId('ff0000'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); let root = new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B), { 'var': new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _C, _B)) }); @@ -286,7 +286,7 @@ suite('Token theme resolving', () => { const _B = colorMap.getId('272822'); const _C = colorMap.getId('ff0000'); const _D = colorMap.getId('00ff00'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); let root = new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B), { 'var': new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _C, _B), { 'identifier': new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _D, _B)) @@ -314,7 +314,7 @@ suite('Token theme resolving', () => { const _E = colorMap.getId('300000'); const _F = colorMap.getId('ff0000'); const _G = colorMap.getId('00ff00'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); let root = new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B), { 'var': new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _F, _B), { 'identifier': new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _G, _B)) @@ -341,6 +341,6 @@ suite('Token theme resolving', () => { colorMap.getId('FFFFFF'); colorMap.getId('0F0F0F'); colorMap.getId('F8F8F2'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); }); }); diff --git a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts index fb4c4028..aaa8f060 100644 --- a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts +++ b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts @@ -31,7 +31,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ]; let expectedStr = `
    ${toStr(expected)}
    `; - assert.equal(actual, expectedStr); + assert.strictEqual(actual, expectedStr); mode.dispose(); }); @@ -61,7 +61,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { let expectedStr2 = toStr(expected2); let expectedStr = `
    ${expectedStr1}
    ${expectedStr2}
    `; - assert.equal(actual, expectedStr); + assert.strictEqual(actual, expectedStr); mode.dispose(); }); @@ -104,7 +104,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ]); const colorMap = [null!, '#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff']; - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 0, 17, 4, true), [ '
    ', @@ -117,7 +117,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 0, 12, 4, true), [ '
    ', @@ -130,7 +130,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 0, 11, 4, true), [ '
    ', @@ -142,7 +142,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 1, 11, 4, true), [ '
    ', @@ -154,7 +154,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 4, 11, 4, true), [ '
    ', @@ -165,7 +165,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 5, 11, 4, true), [ '
    ', @@ -175,7 +175,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 5, 10, 4, true), [ '
    ', @@ -184,7 +184,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 6, 9, 4, true), [ '
    ', @@ -237,7 +237,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ]); const colorMap = [null!, '#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff']; - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 0, 21, 4, true), [ '
    ', @@ -251,7 +251,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 0, 17, 4, true), [ '
    ', @@ -265,7 +265,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 0, 3, 4, true), [ '
    ', @@ -287,7 +287,7 @@ class Mode extends MockMode { this._register(TokenizationRegistry.register(this.getId(), { getInitialState: (): IState => null!, tokenize: undefined!, - tokenize2: (line: string, state: IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: IState): TokenizationResult2 => { let tokensArr: number[] = []; let prevColor: ColorId = -1; for (let i = 0; i < line.length; i++) { diff --git a/src/vs/editor/test/common/services/editorSimpleWorker.test.ts b/src/vs/editor/test/common/services/editorSimpleWorker.test.ts index 54f73ed0..bcfafe9d 100644 --- a/src/vs/editor/test/common/services/editorSimpleWorker.test.ts +++ b/src/vs/editor/test/common/services/editorSimpleWorker.test.ts @@ -43,13 +43,13 @@ suite('EditorSimpleWorker', () => { function assertPositionAt(offset: number, line: number, column: number) { let position = model.positionAt(offset); - assert.equal(position.lineNumber, line); - assert.equal(position.column, column); + assert.strictEqual(position.lineNumber, line); + assert.strictEqual(position.column, column); } function assertOffsetAt(lineNumber: number, column: number, offset: number) { let actual = model.offsetAt({ lineNumber, column }); - assert.equal(actual, offset); + assert.strictEqual(actual, offset); } test('ICommonModel#offsetAt', () => { @@ -83,16 +83,16 @@ suite('EditorSimpleWorker', () => { test('ICommonModel#validatePosition, issue #15882', function () { let model = worker.addModel(['{"id": "0001","type": "donut","name": "Cake","image":{"url": "images/0001.jpg","width": 200,"height": 200},"thumbnail":{"url": "images/thumbnails/0001.jpg","width": 32,"height": 32}}']); - assert.equal(model.offsetAt({ lineNumber: 1, column: 2 }), 1); + assert.strictEqual(model.offsetAt({ lineNumber: 1, column: 2 }), 1); }); test('MoreMinimal', () => { return worker.computeMoreMinimalEdits(model.uri.toString(), [{ text: 'This is line One', range: new Range(1, 1, 1, 17) }]).then(edits => { - assert.equal(edits.length, 1); + assert.strictEqual(edits.length, 1); const [first] = edits; - assert.equal(first.text, 'O'); - assert.deepEqual(first.range, { startLineNumber: 1, startColumn: 14, endLineNumber: 1, endColumn: 15 }); + assert.strictEqual(first.text, 'O'); + assert.deepStrictEqual(first.range, { startLineNumber: 1, startColumn: 14, endLineNumber: 1, endColumn: 15 }); }); }); @@ -105,7 +105,7 @@ suite('EditorSimpleWorker', () => { ], '\n'); return worker.computeMoreMinimalEdits(model.uri.toString(), [{ text: '{\r\n\t"a":1\r\n}', range: new Range(1, 1, 3, 2) }]).then(edits => { - assert.equal(edits.length, 0); + assert.strictEqual(edits.length, 0); }); }); @@ -118,10 +118,10 @@ suite('EditorSimpleWorker', () => { ], '\n'); return worker.computeMoreMinimalEdits(model.uri.toString(), [{ text: '{\r\n\t"b":1\r\n}', range: new Range(1, 1, 3, 2) }]).then(edits => { - assert.equal(edits.length, 1); + assert.strictEqual(edits.length, 1); const [first] = edits; - assert.equal(first.text, 'b'); - assert.deepEqual(first.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 2, endColumn: 4 }); + assert.strictEqual(first.text, 'b'); + assert.deepStrictEqual(first.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 2, endColumn: 4 }); }); }); @@ -134,10 +134,10 @@ suite('EditorSimpleWorker', () => { ]); return worker.computeMoreMinimalEdits(model.uri.toString(), [{ text: '\n', range: new Range(3, 2, 4, 1000) }]).then(edits => { - assert.equal(edits.length, 1); + assert.strictEqual(edits.length, 1); const [first] = edits; - assert.equal(first.text, '\n'); - assert.deepEqual(first.range, { startLineNumber: 3, startColumn: 2, endLineNumber: 3, endColumn: 2 }); + assert.strictEqual(first.text, '\n'); + assert.deepStrictEqual(first.range, { startLineNumber: 3, startColumn: 2, endLineNumber: 3, endColumn: 2 }); }); }); @@ -151,7 +151,7 @@ suite('EditorSimpleWorker', () => { ]); const value = model.getValueInRange({ startLineNumber: 3, startColumn: 1, endLineNumber: 4, endColumn: 1 }); - assert.equal(value, '}'); + assert.strictEqual(value, '}'); }); @@ -165,11 +165,10 @@ suite('EditorSimpleWorker', () => { return worker.textualSuggest([model.uri.toString()], 'f', '[a-z]+', 'img').then((result) => { if (!result) { assert.ok(false); - return; } - assert.equal(result.words.length, 1); - assert.equal(typeof result.duration, 'number'); - assert.equal(result.words[0], 'foobar'); + assert.strictEqual(result.words.length, 1); + assert.strictEqual(typeof result.duration, 'number'); + assert.strictEqual(result.words[0], 'foobar'); }); }); @@ -187,6 +186,6 @@ suite('EditorSimpleWorker', () => { let words: string[] = [...model.words(/[a-z]+/img)]; - assert.deepEqual(words, ['one', 'line', 'two', 'line', 'past', 'empty', 'single', 'and', 'now', 'we', 'are', 'done']); + assert.deepStrictEqual(words, ['one', 'line', 'two', 'line', 'past', 'empty', 'single', 'and', 'now', 'we', 'are', 'done']); }); }); diff --git a/src/vs/editor/test/common/services/languagesRegistry.test.ts b/src/vs/editor/test/common/services/languagesRegistry.test.ts index 09ef74cd..d0eac05b 100644 --- a/src/vs/editor/test/common/services/languagesRegistry.test.ts +++ b/src/vs/editor/test/common/services/languagesRegistry.test.ts @@ -19,7 +19,7 @@ suite('LanguagesRegistry', () => { mimetypes: ['outputModeMimeType'], }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), []); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), []); }); test('mode with alias does have a name', () => { @@ -32,8 +32,8 @@ suite('LanguagesRegistry', () => { mimetypes: ['bla'], }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['ModeName']); - assert.deepEqual(registry.getLanguageName('modeId'), 'ModeName'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['ModeName']); + assert.deepStrictEqual(registry.getLanguageName('modeId'), 'ModeName'); }); test('mode without alias gets a name', () => { @@ -45,8 +45,8 @@ suite('LanguagesRegistry', () => { mimetypes: ['bla'], }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['modeId']); - assert.deepEqual(registry.getLanguageName('modeId'), 'modeId'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['modeId']); + assert.deepStrictEqual(registry.getLanguageName('modeId'), 'modeId'); }); test('bug #4360: f# not shown in status bar', () => { @@ -66,8 +66,8 @@ suite('LanguagesRegistry', () => { mimetypes: ['bla'], }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['ModeName']); - assert.deepEqual(registry.getLanguageName('modeId'), 'ModeName'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['ModeName']); + assert.deepStrictEqual(registry.getLanguageName('modeId'), 'ModeName'); }); test('issue #5278: Extension cannot override language name anymore', () => { @@ -87,8 +87,8 @@ suite('LanguagesRegistry', () => { mimetypes: ['bla'], }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['BetterModeName']); - assert.deepEqual(registry.getLanguageName('modeId'), 'BetterModeName'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['BetterModeName']); + assert.deepStrictEqual(registry.getLanguageName('modeId'), 'BetterModeName'); }); test('mimetypes are generated if necessary', () => { @@ -98,7 +98,7 @@ suite('LanguagesRegistry', () => { id: 'modeId' }]); - assert.deepEqual(registry.getMimeForMode('modeId'), 'text/x-modeId'); + assert.deepStrictEqual(registry.getMimeForMode('modeId'), 'text/x-modeId'); }); test('first mimetype wins', () => { @@ -109,7 +109,7 @@ suite('LanguagesRegistry', () => { mimetypes: ['text/modeId', 'text/modeId2'] }]); - assert.deepEqual(registry.getMimeForMode('modeId'), 'text/modeId'); + assert.deepStrictEqual(registry.getMimeForMode('modeId'), 'text/modeId'); }); test('first mimetype wins 2', () => { @@ -124,7 +124,7 @@ suite('LanguagesRegistry', () => { mimetypes: ['text/modeId'] }]); - assert.deepEqual(registry.getMimeForMode('modeId'), 'text/x-modeId'); + assert.deepStrictEqual(registry.getMimeForMode('modeId'), 'text/x-modeId'); }); test('aliases', () => { @@ -134,42 +134,42 @@ suite('LanguagesRegistry', () => { id: 'a' }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['a']); - assert.deepEqual(registry.getModeIdsFromLanguageName('a'), ['a']); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); - assert.deepEqual(registry.getLanguageName('a'), 'a'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['a']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('a'), ['a']); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); + assert.deepStrictEqual(registry.getLanguageName('a'), 'a'); registry._registerLanguages([{ id: 'a', aliases: ['A1', 'A2'] }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['A1']); - assert.deepEqual(registry.getModeIdsFromLanguageName('a'), []); - assert.deepEqual(registry.getModeIdsFromLanguageName('A1'), ['a']); - assert.deepEqual(registry.getModeIdsFromLanguageName('A2'), []); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a1'), 'a'); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a2'), 'a'); - assert.deepEqual(registry.getLanguageName('a'), 'A1'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['A1']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('a'), []); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A1'), ['a']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A2'), []); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a1'), 'a'); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a2'), 'a'); + assert.deepStrictEqual(registry.getLanguageName('a'), 'A1'); registry._registerLanguages([{ id: 'a', aliases: ['A3', 'A4'] }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['A3']); - assert.deepEqual(registry.getModeIdsFromLanguageName('a'), []); - assert.deepEqual(registry.getModeIdsFromLanguageName('A1'), []); - assert.deepEqual(registry.getModeIdsFromLanguageName('A2'), []); - assert.deepEqual(registry.getModeIdsFromLanguageName('A3'), ['a']); - assert.deepEqual(registry.getModeIdsFromLanguageName('A4'), []); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a1'), 'a'); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a2'), 'a'); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a3'), 'a'); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a4'), 'a'); - assert.deepEqual(registry.getLanguageName('a'), 'A3'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['A3']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('a'), []); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A1'), []); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A2'), []); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A3'), ['a']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A4'), []); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a1'), 'a'); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a2'), 'a'); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a3'), 'a'); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a4'), 'a'); + assert.deepStrictEqual(registry.getLanguageName('a'), 'A3'); }); test('empty aliases array means no alias', () => { @@ -179,23 +179,23 @@ suite('LanguagesRegistry', () => { id: 'a' }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['a']); - assert.deepEqual(registry.getModeIdsFromLanguageName('a'), ['a']); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); - assert.deepEqual(registry.getLanguageName('a'), 'a'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['a']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('a'), ['a']); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); + assert.deepStrictEqual(registry.getLanguageName('a'), 'a'); registry._registerLanguages([{ id: 'b', aliases: [] }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['a']); - assert.deepEqual(registry.getModeIdsFromLanguageName('a'), ['a']); - assert.deepEqual(registry.getModeIdsFromLanguageName('b'), []); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('b'), 'b'); - assert.deepEqual(registry.getLanguageName('a'), 'a'); - assert.deepEqual(registry.getLanguageName('b'), null); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['a']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('a'), ['a']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('b'), []); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('b'), 'b'); + assert.deepStrictEqual(registry.getLanguageName('a'), 'a'); + assert.deepStrictEqual(registry.getLanguageName('b'), null); }); test('extensions', () => { @@ -207,18 +207,18 @@ suite('LanguagesRegistry', () => { extensions: ['aExt'] }]); - assert.deepEqual(registry.getExtensions('a'), []); - assert.deepEqual(registry.getExtensions('aname'), []); - assert.deepEqual(registry.getExtensions('aName'), ['aExt']); + assert.deepStrictEqual(registry.getExtensions('a'), []); + assert.deepStrictEqual(registry.getExtensions('aname'), []); + assert.deepStrictEqual(registry.getExtensions('aName'), ['aExt']); registry._registerLanguages([{ id: 'a', extensions: ['aExt2'] }]); - assert.deepEqual(registry.getExtensions('a'), []); - assert.deepEqual(registry.getExtensions('aname'), []); - assert.deepEqual(registry.getExtensions('aName'), ['aExt', 'aExt2']); + assert.deepStrictEqual(registry.getExtensions('a'), []); + assert.deepStrictEqual(registry.getExtensions('aname'), []); + assert.deepStrictEqual(registry.getExtensions('aName'), ['aExt', 'aExt2']); }); test('extensions of primary language registration come first', () => { @@ -229,7 +229,7 @@ suite('LanguagesRegistry', () => { extensions: ['aExt3'] }]); - assert.deepEqual(registry.getExtensions('a')[0], 'aExt3'); + assert.deepStrictEqual(registry.getExtensions('a')[0], 'aExt3'); registry._registerLanguages([{ id: 'a', @@ -237,14 +237,14 @@ suite('LanguagesRegistry', () => { extensions: ['aExt'] }]); - assert.deepEqual(registry.getExtensions('a')[0], 'aExt'); + assert.deepStrictEqual(registry.getExtensions('a')[0], 'aExt'); registry._registerLanguages([{ id: 'a', extensions: ['aExt2'] }]); - assert.deepEqual(registry.getExtensions('a')[0], 'aExt'); + assert.deepStrictEqual(registry.getExtensions('a')[0], 'aExt'); }); test('filenames', () => { @@ -256,18 +256,18 @@ suite('LanguagesRegistry', () => { filenames: ['aFilename'] }]); - assert.deepEqual(registry.getFilenames('a'), []); - assert.deepEqual(registry.getFilenames('aname'), []); - assert.deepEqual(registry.getFilenames('aName'), ['aFilename']); + assert.deepStrictEqual(registry.getFilenames('a'), []); + assert.deepStrictEqual(registry.getFilenames('aname'), []); + assert.deepStrictEqual(registry.getFilenames('aName'), ['aFilename']); registry._registerLanguages([{ id: 'a', filenames: ['aFilename2'] }]); - assert.deepEqual(registry.getFilenames('a'), []); - assert.deepEqual(registry.getFilenames('aname'), []); - assert.deepEqual(registry.getFilenames('aName'), ['aFilename', 'aFilename2']); + assert.deepStrictEqual(registry.getFilenames('a'), []); + assert.deepStrictEqual(registry.getFilenames('aname'), []); + assert.deepStrictEqual(registry.getFilenames('aName'), ['aFilename', 'aFilename2']); }); test('configuration', () => { @@ -279,17 +279,17 @@ suite('LanguagesRegistry', () => { configuration: URI.file('/path/to/aFilename') }]); - assert.deepEqual(registry.getConfigurationFiles('a'), [URI.file('/path/to/aFilename')]); - assert.deepEqual(registry.getConfigurationFiles('aname'), []); - assert.deepEqual(registry.getConfigurationFiles('aName'), []); + assert.deepStrictEqual(registry.getConfigurationFiles('a'), [URI.file('/path/to/aFilename')]); + assert.deepStrictEqual(registry.getConfigurationFiles('aname'), []); + assert.deepStrictEqual(registry.getConfigurationFiles('aName'), []); registry._registerLanguages([{ id: 'a', configuration: URI.file('/path/to/aFilename2') }]); - assert.deepEqual(registry.getConfigurationFiles('a'), [URI.file('/path/to/aFilename'), URI.file('/path/to/aFilename2')]); - assert.deepEqual(registry.getConfigurationFiles('aname'), []); - assert.deepEqual(registry.getConfigurationFiles('aName'), []); + assert.deepStrictEqual(registry.getConfigurationFiles('a'), [URI.file('/path/to/aFilename'), URI.file('/path/to/aFilename2')]); + assert.deepStrictEqual(registry.getConfigurationFiles('aname'), []); + assert.deepStrictEqual(registry.getConfigurationFiles('aName'), []); }); }); diff --git a/src/vs/editor/test/common/services/modelService.test.ts b/src/vs/editor/test/common/services/modelService.test.ts index 52672ffe..23e0f8f2 100644 --- a/src/vs/editor/test/common/services/modelService.test.ts +++ b/src/vs/editor/test/common/services/modelService.test.ts @@ -11,18 +11,26 @@ 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 { createStringBuilder } from 'vs/editor/common/core/stringBuilder'; -import { DefaultEndOfLine } from 'vs/editor/common/model'; +import { DefaultEndOfLine, ITextModel } from 'vs/editor/common/model'; import { createTextBuffer } from 'vs/editor/common/model/textModel'; -import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; -import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ModelSemanticColoring, ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; +import { TestColorTheme, TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { NullLogService } from 'vs/platform/log/common/log'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DocumentSemanticTokensProvider, DocumentSemanticTokensProviderRegistry, SemanticTokens, SemanticTokensEdits, SemanticTokensLegend } from 'vs/editor/common/modes'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Barrier, timeout } from 'vs/base/common/async'; +import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; +import { ColorScheme } from 'vs/platform/theme/common/theme'; +import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/testTextResourcePropertiesService'; const GENERATE_TESTS = false; @@ -47,9 +55,9 @@ suite('ModelService', () => { const model2 = modelService.createModel('farboo', null, URI.file(platform.isWindows ? 'c:\\myroot\\myfile.txt' : '/myroot/myfile.txt')); const model3 = modelService.createModel('farboo', null, URI.file(platform.isWindows ? 'c:\\other\\myfile.txt' : '/other/myfile.txt')); - assert.equal(model1.getOptions().defaultEOL, DefaultEndOfLine.LF); - assert.equal(model2.getOptions().defaultEOL, DefaultEndOfLine.CRLF); - assert.equal(model3.getOptions().defaultEOL, DefaultEndOfLine.LF); + assert.strictEqual(model1.getOptions().defaultEOL, DefaultEndOfLine.LF); + assert.strictEqual(model2.getOptions().defaultEOL, DefaultEndOfLine.CRLF); + assert.strictEqual(model3.getOptions().defaultEOL, DefaultEndOfLine.LF); }); test('_computeEdits no change', function () { @@ -71,11 +79,11 @@ suite('ModelService', () => { 'and finished with the fourth.', //29 ].join('\n'), DefaultEndOfLine.LF - ); + ).textBuffer; const actual = ModelServiceImpl._computeEdits(model, textBuffer); - assert.deepEqual(actual, []); + assert.deepStrictEqual(actual, []); }); test('_computeEdits first line changed', function () { @@ -97,11 +105,11 @@ suite('ModelService', () => { 'and finished with the fourth.', //29 ].join('\n'), DefaultEndOfLine.LF - ); + ).textBuffer; const actual = ModelServiceImpl._computeEdits(model, textBuffer); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ EditOperation.replaceMove(new Range(1, 1, 2, 1), 'This is line One\n') ]); }); @@ -125,11 +133,11 @@ suite('ModelService', () => { 'and finished with the fourth.', //29 ].join('\r\n'), DefaultEndOfLine.LF - ); + ).textBuffer; const actual = ModelServiceImpl._computeEdits(model, textBuffer); - assert.deepEqual(actual, []); + assert.deepStrictEqual(actual, []); }); test('_computeEdits EOL and other change 1', function () { @@ -151,11 +159,11 @@ suite('ModelService', () => { 'and finished with the fourth.', //29 ].join('\r\n'), DefaultEndOfLine.LF - ); + ).textBuffer; const actual = ModelServiceImpl._computeEdits(model, textBuffer); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ EditOperation.replaceMove( new Range(1, 1, 4, 1), [ @@ -186,11 +194,11 @@ suite('ModelService', () => { '' ].join('\r\n'), DefaultEndOfLine.LF - ); + ).textBuffer; const actual = ModelServiceImpl._computeEdits(model, textBuffer); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ EditOperation.replaceMove(new Range(3, 2, 3, 2), '\r\n') ]); }); @@ -317,7 +325,7 @@ suite('ModelService', () => { const model1 = modelService.createModel('text', null, resource); // make an edit model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]); - assert.equal(model1.getValue(), 'text1'); + assert.strictEqual(model1.getValue(), 'text1'); // dispose it modelService.destroyModel(resource); @@ -325,7 +333,7 @@ suite('ModelService', () => { const model2 = modelService.createModel('text1', null, resource); // undo model2.undo(); - assert.equal(model2.getValue(), 'text'); + assert.strictEqual(model2.getValue(), 'text'); }); test('maintains version id and alternative version id for same resource and same content', () => { @@ -335,7 +343,7 @@ suite('ModelService', () => { const model1 = modelService.createModel('text', null, resource); // make an edit model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]); - assert.equal(model1.getValue(), 'text1'); + assert.strictEqual(model1.getValue(), 'text1'); const versionId = model1.getVersionId(); const alternativeVersionId = model1.getAlternativeVersionId(); // dispose it @@ -343,8 +351,8 @@ suite('ModelService', () => { // create a new model with the same content const model2 = modelService.createModel('text1', null, resource); - assert.equal(model2.getVersionId(), versionId); - assert.equal(model2.getAlternativeVersionId(), alternativeVersionId); + assert.strictEqual(model2.getVersionId(), versionId); + assert.strictEqual(model2.getAlternativeVersionId(), alternativeVersionId); }); test('does not maintain undo for same resource and different content', () => { @@ -354,7 +362,7 @@ suite('ModelService', () => { const model1 = modelService.createModel('text', null, resource); // make an edit model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]); - assert.equal(model1.getValue(), 'text1'); + assert.strictEqual(model1.getValue(), 'text1'); // dispose it modelService.destroyModel(resource); @@ -362,7 +370,7 @@ suite('ModelService', () => { const model2 = modelService.createModel('text2', null, resource); // undo model2.undo(); - assert.equal(model2.getValue(), 'text2'); + assert.strictEqual(model2.getValue(), 'text2'); }); test('setValue should clear undo stack', () => { @@ -370,17 +378,98 @@ suite('ModelService', () => { const model = modelService.createModel('text', null, resource); model.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]); - assert.equal(model.getValue(), 'text1'); + assert.strictEqual(model.getValue(), 'text1'); model.setValue('text2'); model.undo(); - assert.equal(model.getValue(), 'text2'); + assert.strictEqual(model.getValue(), 'text2'); + }); +}); + +suite('ModelSemanticColoring', () => { + + const disposables = new DisposableStore(); + const ORIGINAL_FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY = ModelSemanticColoring.FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY; + let modelService: IModelService; + let modeService: IModeService; + + setup(() => { + ModelSemanticColoring.FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY = 0; + + const configService = new TestConfigurationService({ editor: { semanticHighlighting: true } }); + const themeService = new TestThemeService(); + themeService.setTheme(new TestColorTheme({}, ColorScheme.DARK, true)); + modelService = disposables.add(new ModelServiceImpl( + configService, + new TestTextResourcePropertiesService(configService), + themeService, + new NullLogService(), + new UndoRedoService(new TestDialogService(), new TestNotificationService()) + )); + modeService = disposables.add(new ModeServiceImpl(false)); + }); + + teardown(() => { + disposables.clear(); + ModelSemanticColoring.FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY = ORIGINAL_FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY; + }); + + test('DocumentSemanticTokens should be fetched when the result is empty if there are pending changes', async () => { + + disposables.add(ModesRegistry.registerLanguage({ id: 'testMode' })); + + const inFirstCall = new Barrier(); + const delayFirstResult = new Barrier(); + const secondResultProvided = new Barrier(); + let callCount = 0; + + disposables.add(DocumentSemanticTokensProviderRegistry.register('testMode', new class implements DocumentSemanticTokensProvider { + getLegend(): SemanticTokensLegend { + return { tokenTypes: ['class'], tokenModifiers: [] }; + } + async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise { + callCount++; + if (callCount === 1) { + assert.ok('called once'); + inFirstCall.open(); + await delayFirstResult.wait(); + await timeout(0); // wait for the simple scheduler to fire to check that we do actually get rescheduled + return null; + } + if (callCount === 2) { + assert.ok('called twice'); + secondResultProvided.open(); + return null; + } + assert.fail('Unexpected call'); + } + releaseDocumentSemanticTokens(resultId: string | undefined): void { + } + })); + + const textModel = disposables.add(modelService.createModel('Hello world', modeService.create('testMode'))); + + // wait for the provider to be called + await inFirstCall.wait(); + + // the provider is now in the provide call + // change the text buffer while the provider is running + textModel.applyEdits([{ range: new Range(1, 1, 1, 1), text: 'x' }]); + + // let the provider finish its first result + delayFirstResult.open(); + + // we need to check that the provider is called again, even if it returns null + await secondResultProvided.wait(); + + // assert that it got called twice + assert.strictEqual(callCount, 2); }); }); function assertComputeEdits(lines1: string[], lines2: string[]): void { const model = createTextModel(lines1.join('\n')); - const textBuffer = createTextBuffer(lines2.join('\n'), DefaultEndOfLine.LF); + const textBuffer = createTextBuffer(lines2.join('\n'), DefaultEndOfLine.LF).textBuffer; // compute required edits // let start = Date.now(); @@ -390,7 +479,7 @@ function assertComputeEdits(lines1: string[], lines2: string[]): void { // apply edits model.pushEditOperations([], edits, null); - assert.equal(model.getValue(), lines2.join('\n')); + assert.strictEqual(model.getValue(), lines2.join('\n')); } function getRandomInt(min: number, max: number): number { @@ -439,21 +528,3 @@ assertComputeEdits(file1, file2); } } } - -export class TestTextResourcePropertiesService implements ITextResourcePropertiesService { - - declare readonly _serviceBrand: undefined; - - constructor( - @IConfigurationService private readonly configurationService: IConfigurationService, - ) { - } - - getEOL(resource: URI, language?: string): string { - const eol = this.configurationService.getValue('files.eol', { overrideIdentifier: language, resource }); - if (eol && eol !== 'auto') { - return eol; - } - return (platform.isLinux || platform.isMacintosh) ? '\n' : '\r\n'; - } -} diff --git a/src/vs/workbench/test/common/api/semanticTokensDto.test.ts b/src/vs/editor/test/common/services/semanticTokensDto.test.ts similarity index 95% rename from src/vs/workbench/test/common/api/semanticTokensDto.test.ts rename to src/vs/editor/test/common/services/semanticTokensDto.test.ts index 091bc24e..9cedebd2 100644 --- a/src/vs/workbench/test/common/api/semanticTokensDto.test.ts +++ b/src/vs/editor/test/common/services/semanticTokensDto.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IFullSemanticTokensDto, IDeltaSemanticTokensDto, encodeSemanticTokensDto, ISemanticTokensDto, decodeSemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokensDto'; +import { IFullSemanticTokensDto, IDeltaSemanticTokensDto, encodeSemanticTokensDto, ISemanticTokensDto, decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; import { VSBuffer } from 'vs/base/common/buffer'; suite('SemanticTokensDto', () => { @@ -25,7 +25,7 @@ suite('SemanticTokensDto', () => { data: toArr(dto.data) }; }; - assert.deepEqual(convert(actual), convert(expected)); + assert.deepStrictEqual(convert(actual), convert(expected)); } function assertEqualDelta(actual: IDeltaSemanticTokensDto, expected: IDeltaSemanticTokensDto): void { @@ -46,7 +46,7 @@ suite('SemanticTokensDto', () => { deltas: dto.deltas.map(convertOne) }; }; - assert.deepEqual(convert(actual), convert(expected)); + assert.deepStrictEqual(convert(actual), convert(expected)); } function testRoundTrip(value: ISemanticTokensDto): void { diff --git a/src/vs/editor/test/common/services/testTextResourcePropertiesService.ts b/src/vs/editor/test/common/services/testTextResourcePropertiesService.ts new file mode 100644 index 00000000..f5ac31d5 --- /dev/null +++ b/src/vs/editor/test/common/services/testTextResourcePropertiesService.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as platform from 'vs/base/common/platform'; +import { URI } from 'vs/base/common/uri'; +import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + +export class TestTextResourcePropertiesService implements ITextResourcePropertiesService { + + declare readonly _serviceBrand: undefined; + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + ) { + } + + getEOL(resource: URI, language?: string): string { + const eol = this.configurationService.getValue('files.eol', { overrideIdentifier: language, resource }); + if (eol && eol !== 'auto') { + return eol; + } + return (platform.isLinux || platform.isMacintosh) ? '\n' : '\r\n'; + } +} diff --git a/src/vs/editor/test/common/view/overviewZoneManager.test.ts b/src/vs/editor/test/common/view/overviewZoneManager.test.ts index 39c104fb..ee8c8ed7 100644 --- a/src/vs/editor/test/common/view/overviewZoneManager.test.ts +++ b/src/vs/editor/test/common/view/overviewZoneManager.test.ts @@ -26,7 +26,7 @@ suite('Editor View - OverviewZoneManager', () => { ]); // one line = 12, but cap is at 6 - assert.deepEqual(manager.resolveColorZones(), [ + assert.deepStrictEqual(manager.resolveColorZones(), [ new ColorZone(12, 24, 1), // new ColorZone(120, 132, 2), // 120 -> 132 new ColorZone(360, 384, 3), // 360 -> 372 [360 -> 384] @@ -52,7 +52,7 @@ suite('Editor View - OverviewZoneManager', () => { ]); // one line = 6, cap is at 6 - assert.deepEqual(manager.resolveColorZones(), [ + assert.deepStrictEqual(manager.resolveColorZones(), [ new ColorZone(6, 12, 1), // new ColorZone(60, 66, 2), // 60 -> 66 new ColorZone(180, 192, 3), // 180 -> 192 @@ -78,7 +78,7 @@ suite('Editor View - OverviewZoneManager', () => { ]); // one line = 6, cap is at 12 - assert.deepEqual(manager.resolveColorZones(), [ + assert.deepStrictEqual(manager.resolveColorZones(), [ new ColorZone(12, 24, 1), // new ColorZone(120, 132, 2), // 120 -> 132 new ColorZone(360, 384, 3), // 360 -> 384 diff --git a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts index 910165a0..a0fa69bc 100644 --- a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts +++ b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts @@ -95,7 +95,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { maxDigitWidth: input.maxDigitWidth, pixelRatio: input.pixelRatio, }); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } test('EditorLayoutProvider 1', () => { diff --git a/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts b/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts index 8be7627b..e8939e78 100644 --- a/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts +++ b/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts @@ -17,7 +17,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular) ]); - assert.deepEqual(result, [ + assert.deepStrictEqual(result, [ new DecorationSegment(0, 1, 'c1', 0), new DecorationSegment(2, 2, 'c2 c1', 0), new DecorationSegment(3, 9, 'c1', 0), @@ -31,7 +31,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new LineDecoration(20, 21, 'inline-folded', InlineDecorationType.Regular), ]); - assert.deepEqual(result, [ + assert.deepStrictEqual(result, [ new DecorationSegment(14, 18, 'mtkw', 0), new DecorationSegment(19, 19, 'mtkw inline-folded', 0) ]); @@ -43,7 +43,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new InlineDecoration(new Range(2, 12, 3, 30), 'detected-link', InlineDecorationType.Regular) ], 3, 12, 500); - assert.deepEqual(result, [ + assert.deepStrictEqual(result, [ new LineDecoration(12, 30, 'detected-link', InlineDecorationType.Regular), ]); }); @@ -54,7 +54,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new InlineDecoration(new Range(4, 0, 4, 1), 'after', InlineDecorationType.After), ], 4, 1, 500); - assert.deepEqual(result, [ + assert.deepStrictEqual(result, [ new LineDecoration(1, 2, 'before', InlineDecorationType.Before), new LineDecoration(0, 1, 'after', InlineDecorationType.After), ]); @@ -62,7 +62,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { test('ViewLineParts', () => { - assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ + assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 2, 'c1', InlineDecorationType.Regular), new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular) ]), [ @@ -70,7 +70,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new DecorationSegment(2, 2, 'c2', 0) ]); - assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ + assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 3, 'c1', InlineDecorationType.Regular), new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular) ]), [ @@ -78,7 +78,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new DecorationSegment(2, 2, 'c2', 0) ]); - assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ + assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 4, 'c1', InlineDecorationType.Regular), new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular) ]), [ @@ -86,7 +86,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new DecorationSegment(2, 2, 'c1 c2', 0) ]); - assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ + assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 4, 'c1', InlineDecorationType.Regular), new LineDecoration(1, 4, 'c1*', InlineDecorationType.Regular), new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular) @@ -95,7 +95,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new DecorationSegment(2, 2, 'c1 c1* c2', 0) ]); - assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ + assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 4, 'c1', InlineDecorationType.Regular), new LineDecoration(1, 4, 'c1*', InlineDecorationType.Regular), new LineDecoration(1, 4, 'c1**', InlineDecorationType.Regular), @@ -105,7 +105,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new DecorationSegment(2, 2, 'c1 c1* c1** c2', 0) ]); - assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ + assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 4, 'c1', InlineDecorationType.Regular), new LineDecoration(1, 4, 'c1*', InlineDecorationType.Regular), new LineDecoration(1, 4, 'c1**', InlineDecorationType.Regular), @@ -116,7 +116,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new DecorationSegment(2, 2, 'c1 c1* c1** c2 c2*', 0) ]); - assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ + assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 4, 'c1', InlineDecorationType.Regular), new LineDecoration(1, 4, 'c1*', InlineDecorationType.Regular), new LineDecoration(1, 4, 'c1**', InlineDecorationType.Regular), diff --git a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts index 3205a10c..6d619cf6 100644 --- a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts +++ b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts @@ -34,105 +34,105 @@ suite('Editor ViewLayout - LinesLayout', () => { // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: - - assert.equal(linesLayout.getLinesTotalHeight(), 100); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 10); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 30); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 40); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 50); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 60); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 70); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 80); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 90); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 100); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 10); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 30); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 40); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 50); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 60); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 70); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 80); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 90); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(5), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(11), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(19), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(21), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(29), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(5), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(11), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(19), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(21), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(29), 3); // Add whitespace of height 5px after 2nd line insertWhitespace(linesLayout, 2, 0, 5, 0); // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: a(2,5) - assert.equal(linesLayout.getLinesTotalHeight(), 105); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 10); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 25); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 35); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 45); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 105); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 10); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 25); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 35); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 45); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(21), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(24), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(25), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(45), 5); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(104), 10); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(105), 10); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(21), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(24), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(25), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(45), 5); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(104), 10); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(105), 10); // Add two more whitespaces of height 5px insertWhitespace(linesLayout, 3, 0, 5, 0); insertWhitespace(linesLayout, 4, 0, 5, 0); // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: a(2,5), b(3, 5), c(4, 5) - assert.equal(linesLayout.getLinesTotalHeight(), 115); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 10); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 25); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 40); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 55); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 65); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 115); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 10); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 25); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 40); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 55); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 65); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(19), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(34), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(49), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(50), 5); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(64), 5); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(65), 6); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(19), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(34), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(49), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(50), 5); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(64), 5); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(65), 6); - assert.equal(linesLayout.getVerticalOffsetForWhitespaceIndex(0), 20); // 20 -> 25 - assert.equal(linesLayout.getVerticalOffsetForWhitespaceIndex(1), 35); // 35 -> 40 - assert.equal(linesLayout.getVerticalOffsetForWhitespaceIndex(2), 50); + assert.strictEqual(linesLayout.getVerticalOffsetForWhitespaceIndex(0), 20); // 20 -> 25 + assert.strictEqual(linesLayout.getVerticalOffsetForWhitespaceIndex(1), 35); // 35 -> 40 + assert.strictEqual(linesLayout.getVerticalOffsetForWhitespaceIndex(2), 50); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(0), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(19), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(20), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(21), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(22), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(23), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(24), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(25), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(26), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(34), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(35), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(36), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(39), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(40), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(41), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(49), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(50), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(51), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(54), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(55), -1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(1000), -1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(0), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(19), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(20), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(21), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(22), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(23), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(24), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(25), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(26), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(34), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(35), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(36), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(39), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(40), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(41), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(49), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(50), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(51), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(54), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(55), -1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(1000), -1); }); @@ -144,94 +144,94 @@ suite('Editor ViewLayout - LinesLayout', () => { // 10 lines // whitespace: - a(2,5) - assert.equal(linesLayout.getLinesTotalHeight(), 15); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 7); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 8); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 9); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 10); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 11); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 12); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 13); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 14); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 15); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 7); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 8); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 9); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 10); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 11); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 12); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 13); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 14); // Change whitespace height // 10 lines // whitespace: - a(2,10) changeOneWhitespace(linesLayout, a, 2, 10); - assert.equal(linesLayout.getLinesTotalHeight(), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 12); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 13); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 14); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 15); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 16); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 17); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 18); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 19); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 12); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 13); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 14); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 15); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 16); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 17); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 18); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 19); // Change whitespace position // 10 lines // whitespace: - a(5,10) changeOneWhitespace(linesLayout, a, 5, 10); - assert.equal(linesLayout.getLinesTotalHeight(), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 2); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 3); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 4); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 15); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 16); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 17); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 18); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 19); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 2); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 3); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 4); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 15); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 16); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 17); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 18); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 19); // Pretend that lines 5 and 6 were deleted // 8 lines // whitespace: - a(4,10) linesLayout.onLinesDeleted(5, 6); - assert.equal(linesLayout.getLinesTotalHeight(), 18); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 2); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 3); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 14); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 15); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 16); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 17); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 18); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 2); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 3); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 14); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 15); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 16); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 17); // Insert two lines at the beginning // 10 lines // whitespace: - a(6,10) linesLayout.onLinesInserted(1, 2); - assert.equal(linesLayout.getLinesTotalHeight(), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 2); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 3); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 4); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 5); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 16); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 17); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 18); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 19); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 2); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 3); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 4); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 5); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 16); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 17); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 18); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 19); // Remove whitespace // 10 lines removeWhitespace(linesLayout, a); - assert.equal(linesLayout.getLinesTotalHeight(), 10); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 2); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 3); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 4); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 5); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 6); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 7); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 8); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 9); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 10); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 2); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 3); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 4); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 5); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 6); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 7); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 8); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 9); }); test('LinesLayout Padding', () => { @@ -240,93 +240,93 @@ suite('Editor ViewLayout - LinesLayout', () => { // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: - - assert.equal(linesLayout.getLinesTotalHeight(), 135); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 15); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 25); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 35); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 45); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 55); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 65); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 75); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 85); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 95); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 105); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 135); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 15); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 25); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 35); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 45); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 55); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 65); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 75); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 85); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 95); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 105); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(24), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(25), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(34), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(24), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(25), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(34), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 3); // Add whitespace of height 5px after 2nd line insertWhitespace(linesLayout, 2, 0, 5, 0); // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: a(2,5) - assert.equal(linesLayout.getLinesTotalHeight(), 140); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 15); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 25); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 40); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 50); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 140); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 15); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 25); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 40); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 50); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(25), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(34), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(39), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(40), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(41), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(49), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(50), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(25), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(34), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(39), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(40), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(41), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(49), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(50), 4); // Add two more whitespaces of height 5px insertWhitespace(linesLayout, 3, 0, 5, 0); insertWhitespace(linesLayout, 4, 0, 5, 0); // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: a(2,5), b(3, 5), c(4, 5) - assert.equal(linesLayout.getLinesTotalHeight(), 150); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 15); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 25); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 40); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 55); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 70); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 80); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 150); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 15); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 25); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 40); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 55); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 70); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 80); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(24), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(30), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(39), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(40), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(49), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(50), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(54), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(55), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(64), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(65), 5); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(69), 5); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(70), 5); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(80), 6); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(24), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(30), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(39), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(40), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(49), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(50), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(54), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(55), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(64), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(65), 5); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(69), 5); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(70), 5); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(80), 6); - assert.equal(linesLayout.getVerticalOffsetForWhitespaceIndex(0), 35); // 35 -> 40 - assert.equal(linesLayout.getVerticalOffsetForWhitespaceIndex(1), 50); // 50 -> 55 - assert.equal(linesLayout.getVerticalOffsetForWhitespaceIndex(2), 65); + assert.strictEqual(linesLayout.getVerticalOffsetForWhitespaceIndex(0), 35); // 35 -> 40 + assert.strictEqual(linesLayout.getVerticalOffsetForWhitespaceIndex(1), 50); // 50 -> 55 + assert.strictEqual(linesLayout.getVerticalOffsetForWhitespaceIndex(2), 65); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(0), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(34), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(35), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(39), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(40), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(49), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(50), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(54), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(55), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(64), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(65), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(70), -1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(0), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(34), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(35), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(39), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(40), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(49), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(50), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(54), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(55), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(64), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(65), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(70), -1); }); test('LinesLayout getLineNumberAtOrAfterVerticalOffset', () => { @@ -335,47 +335,47 @@ suite('Editor ViewLayout - LinesLayout', () => { // 10 lines // whitespace: - a(6,10) - assert.equal(linesLayout.getLinesTotalHeight(), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 2); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 3); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 4); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 5); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 16); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 17); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 18); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 19); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 2); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 3); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 4); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 5); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 16); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 17); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 18); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 19); // Do some hit testing // line [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // vertical: [0, 1, 2, 3, 4, 5, 16, 17, 18, 19] - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(-100), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(-1), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(2), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(3), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(4), 5); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(5), 6); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(6), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(7), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(8), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(11), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(12), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(13), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(14), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(16), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(17), 8); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(18), 9); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(19), 10); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 10); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(21), 10); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(22), 10); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(23), 10); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(-100), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(-1), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(2), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(3), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(4), 5); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(5), 6); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(6), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(7), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(8), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(11), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(12), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(13), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(14), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(16), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(17), 8); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(18), 9); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(19), 10); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 10); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(21), 10); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(22), 10); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(23), 10); }); test('LinesLayout getCenteredLineInViewport', () => { @@ -384,81 +384,81 @@ suite('Editor ViewLayout - LinesLayout', () => { // 10 lines // whitespace: - a(6,10) - assert.equal(linesLayout.getLinesTotalHeight(), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 2); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 3); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 4); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 5); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 16); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 17); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 18); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 19); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 2); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 3); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 4); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 5); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 16); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 17); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 18); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 19); // Find centered line in viewport 1 // line [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // vertical: [0, 1, 2, 3, 4, 5, 16, 17, 18, 19] - assert.equal(linesLayout.getLinesViewportData(0, 1).centeredLineNumber, 1); - assert.equal(linesLayout.getLinesViewportData(0, 2).centeredLineNumber, 2); - assert.equal(linesLayout.getLinesViewportData(0, 3).centeredLineNumber, 2); - assert.equal(linesLayout.getLinesViewportData(0, 4).centeredLineNumber, 3); - assert.equal(linesLayout.getLinesViewportData(0, 5).centeredLineNumber, 3); - assert.equal(linesLayout.getLinesViewportData(0, 6).centeredLineNumber, 4); - assert.equal(linesLayout.getLinesViewportData(0, 7).centeredLineNumber, 4); - assert.equal(linesLayout.getLinesViewportData(0, 8).centeredLineNumber, 5); - assert.equal(linesLayout.getLinesViewportData(0, 9).centeredLineNumber, 5); - assert.equal(linesLayout.getLinesViewportData(0, 10).centeredLineNumber, 6); - assert.equal(linesLayout.getLinesViewportData(0, 11).centeredLineNumber, 6); - assert.equal(linesLayout.getLinesViewportData(0, 12).centeredLineNumber, 6); - assert.equal(linesLayout.getLinesViewportData(0, 13).centeredLineNumber, 6); - assert.equal(linesLayout.getLinesViewportData(0, 14).centeredLineNumber, 6); - assert.equal(linesLayout.getLinesViewportData(0, 15).centeredLineNumber, 6); - assert.equal(linesLayout.getLinesViewportData(0, 16).centeredLineNumber, 6); - assert.equal(linesLayout.getLinesViewportData(0, 17).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 18).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 19).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 21).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 22).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 23).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 24).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 25).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 26).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 27).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 28).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 29).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 30).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 31).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 32).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 33).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 1).centeredLineNumber, 1); + assert.strictEqual(linesLayout.getLinesViewportData(0, 2).centeredLineNumber, 2); + assert.strictEqual(linesLayout.getLinesViewportData(0, 3).centeredLineNumber, 2); + assert.strictEqual(linesLayout.getLinesViewportData(0, 4).centeredLineNumber, 3); + assert.strictEqual(linesLayout.getLinesViewportData(0, 5).centeredLineNumber, 3); + assert.strictEqual(linesLayout.getLinesViewportData(0, 6).centeredLineNumber, 4); + assert.strictEqual(linesLayout.getLinesViewportData(0, 7).centeredLineNumber, 4); + assert.strictEqual(linesLayout.getLinesViewportData(0, 8).centeredLineNumber, 5); + assert.strictEqual(linesLayout.getLinesViewportData(0, 9).centeredLineNumber, 5); + assert.strictEqual(linesLayout.getLinesViewportData(0, 10).centeredLineNumber, 6); + assert.strictEqual(linesLayout.getLinesViewportData(0, 11).centeredLineNumber, 6); + assert.strictEqual(linesLayout.getLinesViewportData(0, 12).centeredLineNumber, 6); + assert.strictEqual(linesLayout.getLinesViewportData(0, 13).centeredLineNumber, 6); + assert.strictEqual(linesLayout.getLinesViewportData(0, 14).centeredLineNumber, 6); + assert.strictEqual(linesLayout.getLinesViewportData(0, 15).centeredLineNumber, 6); + assert.strictEqual(linesLayout.getLinesViewportData(0, 16).centeredLineNumber, 6); + assert.strictEqual(linesLayout.getLinesViewportData(0, 17).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 18).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 19).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 21).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 22).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 23).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 24).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 25).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 26).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 27).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 28).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 29).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 30).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 31).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 32).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 33).centeredLineNumber, 7); // Find centered line in viewport 2 // line [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // vertical: [0, 1, 2, 3, 4, 5, 16, 17, 18, 19] - assert.equal(linesLayout.getLinesViewportData(0, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(1, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(2, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(3, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(4, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(5, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(6, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(7, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(8, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(9, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(10, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(11, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(12, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(13, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(14, 20).centeredLineNumber, 8); - assert.equal(linesLayout.getLinesViewportData(15, 20).centeredLineNumber, 8); - assert.equal(linesLayout.getLinesViewportData(16, 20).centeredLineNumber, 9); - assert.equal(linesLayout.getLinesViewportData(17, 20).centeredLineNumber, 9); - assert.equal(linesLayout.getLinesViewportData(18, 20).centeredLineNumber, 10); - assert.equal(linesLayout.getLinesViewportData(19, 20).centeredLineNumber, 10); - assert.equal(linesLayout.getLinesViewportData(20, 23).centeredLineNumber, 10); - assert.equal(linesLayout.getLinesViewportData(21, 23).centeredLineNumber, 10); - assert.equal(linesLayout.getLinesViewportData(22, 23).centeredLineNumber, 10); + assert.strictEqual(linesLayout.getLinesViewportData(0, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(1, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(2, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(3, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(4, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(5, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(6, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(7, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(8, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(9, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(10, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(11, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(12, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(13, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(14, 20).centeredLineNumber, 8); + assert.strictEqual(linesLayout.getLinesViewportData(15, 20).centeredLineNumber, 8); + assert.strictEqual(linesLayout.getLinesViewportData(16, 20).centeredLineNumber, 9); + assert.strictEqual(linesLayout.getLinesViewportData(17, 20).centeredLineNumber, 9); + assert.strictEqual(linesLayout.getLinesViewportData(18, 20).centeredLineNumber, 10); + assert.strictEqual(linesLayout.getLinesViewportData(19, 20).centeredLineNumber, 10); + assert.strictEqual(linesLayout.getLinesViewportData(20, 23).centeredLineNumber, 10); + assert.strictEqual(linesLayout.getLinesViewportData(21, 23).centeredLineNumber, 10); + assert.strictEqual(linesLayout.getLinesViewportData(22, 23).centeredLineNumber, 10); }); test('LinesLayout getLinesViewportData 1', () => { @@ -467,131 +467,131 @@ suite('Editor ViewLayout - LinesLayout', () => { // 10 lines // whitespace: - a(6,100) - assert.equal(linesLayout.getLinesTotalHeight(), 200); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 10); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 30); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 40); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 50); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 160); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 170); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 180); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 190); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 200); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 10); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 30); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 40); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 50); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 160); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 170); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 180); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 190); // viewport 0->50 let viewportData = linesLayout.getLinesViewportData(0, 50); - assert.equal(viewportData.startLineNumber, 1); - assert.equal(viewportData.endLineNumber, 5); - assert.equal(viewportData.completelyVisibleStartLineNumber, 1); - assert.equal(viewportData.completelyVisibleEndLineNumber, 5); - assert.deepEqual(viewportData.relativeVerticalOffset, [0, 10, 20, 30, 40]); + assert.strictEqual(viewportData.startLineNumber, 1); + assert.strictEqual(viewportData.endLineNumber, 5); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 1); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 5); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [0, 10, 20, 30, 40]); // viewport 1->51 viewportData = linesLayout.getLinesViewportData(1, 51); - assert.equal(viewportData.startLineNumber, 1); - assert.equal(viewportData.endLineNumber, 6); - assert.equal(viewportData.completelyVisibleStartLineNumber, 2); - assert.equal(viewportData.completelyVisibleEndLineNumber, 5); - assert.deepEqual(viewportData.relativeVerticalOffset, [0, 10, 20, 30, 40, 50]); + assert.strictEqual(viewportData.startLineNumber, 1); + assert.strictEqual(viewportData.endLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 2); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 5); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [0, 10, 20, 30, 40, 50]); // viewport 5->55 viewportData = linesLayout.getLinesViewportData(5, 55); - assert.equal(viewportData.startLineNumber, 1); - assert.equal(viewportData.endLineNumber, 6); - assert.equal(viewportData.completelyVisibleStartLineNumber, 2); - assert.equal(viewportData.completelyVisibleEndLineNumber, 5); - assert.deepEqual(viewportData.relativeVerticalOffset, [0, 10, 20, 30, 40, 50]); + assert.strictEqual(viewportData.startLineNumber, 1); + assert.strictEqual(viewportData.endLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 2); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 5); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [0, 10, 20, 30, 40, 50]); // viewport 10->60 viewportData = linesLayout.getLinesViewportData(10, 60); - assert.equal(viewportData.startLineNumber, 2); - assert.equal(viewportData.endLineNumber, 6); - assert.equal(viewportData.completelyVisibleStartLineNumber, 2); - assert.equal(viewportData.completelyVisibleEndLineNumber, 6); - assert.deepEqual(viewportData.relativeVerticalOffset, [10, 20, 30, 40, 50]); + assert.strictEqual(viewportData.startLineNumber, 2); + assert.strictEqual(viewportData.endLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 2); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 6); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [10, 20, 30, 40, 50]); // viewport 50->100 viewportData = linesLayout.getLinesViewportData(50, 100); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 6); - assert.equal(viewportData.completelyVisibleStartLineNumber, 6); - assert.equal(viewportData.completelyVisibleEndLineNumber, 6); - assert.deepEqual(viewportData.relativeVerticalOffset, [50]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 6); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50]); // viewport 60->110 viewportData = linesLayout.getLinesViewportData(60, 110); - assert.equal(viewportData.startLineNumber, 7); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 7); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [160]); + assert.strictEqual(viewportData.startLineNumber, 7); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [160]); // viewport 65->115 viewportData = linesLayout.getLinesViewportData(65, 115); - assert.equal(viewportData.startLineNumber, 7); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 7); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [160]); + assert.strictEqual(viewportData.startLineNumber, 7); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [160]); // viewport 50->159 viewportData = linesLayout.getLinesViewportData(50, 159); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 6); - assert.equal(viewportData.completelyVisibleStartLineNumber, 6); - assert.equal(viewportData.completelyVisibleEndLineNumber, 6); - assert.deepEqual(viewportData.relativeVerticalOffset, [50]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 6); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50]); // viewport 50->160 viewportData = linesLayout.getLinesViewportData(50, 160); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 6); - assert.equal(viewportData.completelyVisibleStartLineNumber, 6); - assert.equal(viewportData.completelyVisibleEndLineNumber, 6); - assert.deepEqual(viewportData.relativeVerticalOffset, [50]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 6); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50]); // viewport 51->161 viewportData = linesLayout.getLinesViewportData(51, 161); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 7); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [50, 160]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50, 160]); // viewport 150->169 viewportData = linesLayout.getLinesViewportData(150, 169); - assert.equal(viewportData.startLineNumber, 7); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 7); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [160]); + assert.strictEqual(viewportData.startLineNumber, 7); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [160]); // viewport 159->169 viewportData = linesLayout.getLinesViewportData(159, 169); - assert.equal(viewportData.startLineNumber, 7); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 7); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [160]); + assert.strictEqual(viewportData.startLineNumber, 7); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [160]); // viewport 160->169 viewportData = linesLayout.getLinesViewportData(160, 169); - assert.equal(viewportData.startLineNumber, 7); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 7); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [160]); + assert.strictEqual(viewportData.startLineNumber, 7); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [160]); // viewport 160->1000 viewportData = linesLayout.getLinesViewportData(160, 1000); - assert.equal(viewportData.startLineNumber, 7); - assert.equal(viewportData.endLineNumber, 10); - assert.equal(viewportData.completelyVisibleStartLineNumber, 7); - assert.equal(viewportData.completelyVisibleEndLineNumber, 10); - assert.deepEqual(viewportData.relativeVerticalOffset, [160, 170, 180, 190]); + assert.strictEqual(viewportData.startLineNumber, 7); + assert.strictEqual(viewportData.endLineNumber, 10); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 10); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [160, 170, 180, 190]); }); test('LinesLayout getLinesViewportData 2 & getWhitespaceViewportData', () => { @@ -601,27 +601,27 @@ suite('Editor ViewLayout - LinesLayout', () => { // 10 lines // whitespace: - a(6,100), b(7, 50) - assert.equal(linesLayout.getLinesTotalHeight(), 250); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 10); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 30); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 40); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 50); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 160); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 220); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 230); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 240); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 250); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 10); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 30); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 40); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 50); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 160); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 220); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 230); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 240); // viewport 50->160 let viewportData = linesLayout.getLinesViewportData(50, 160); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 6); - assert.equal(viewportData.completelyVisibleStartLineNumber, 6); - assert.equal(viewportData.completelyVisibleEndLineNumber, 6); - assert.deepEqual(viewportData.relativeVerticalOffset, [50]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 6); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50]); let whitespaceData = linesLayout.getWhitespaceViewportData(50, 160); - assert.deepEqual(whitespaceData, [{ + assert.deepStrictEqual(whitespaceData, [{ id: a, afterLineNumber: 6, verticalOffset: 60, @@ -630,13 +630,13 @@ suite('Editor ViewLayout - LinesLayout', () => { // viewport 50->219 viewportData = linesLayout.getLinesViewportData(50, 219); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 6); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [50, 160]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50, 160]); whitespaceData = linesLayout.getWhitespaceViewportData(50, 219); - assert.deepEqual(whitespaceData, [{ + assert.deepStrictEqual(whitespaceData, [{ id: a, afterLineNumber: 6, verticalOffset: 60, @@ -650,19 +650,19 @@ suite('Editor ViewLayout - LinesLayout', () => { // viewport 50->220 viewportData = linesLayout.getLinesViewportData(50, 220); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 6); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [50, 160]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50, 160]); // viewport 50->250 viewportData = linesLayout.getLinesViewportData(50, 250); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 10); - assert.equal(viewportData.completelyVisibleStartLineNumber, 6); - assert.equal(viewportData.completelyVisibleEndLineNumber, 10); - assert.deepEqual(viewportData.relativeVerticalOffset, [50, 160, 220, 230, 240]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 10); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 10); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50, 160, 220, 230, 240]); }); test('LinesLayout getWhitespaceAtVerticalOffset', () => { @@ -671,40 +671,40 @@ suite('Editor ViewLayout - LinesLayout', () => { let b = insertWhitespace(linesLayout, 7, 0, 50, 0); let whitespace = linesLayout.getWhitespaceAtVerticalOffset(0); - assert.equal(whitespace, null); + assert.strictEqual(whitespace, null); whitespace = linesLayout.getWhitespaceAtVerticalOffset(59); - assert.equal(whitespace, null); + assert.strictEqual(whitespace, null); whitespace = linesLayout.getWhitespaceAtVerticalOffset(60); - assert.equal(whitespace!.id, a); + assert.strictEqual(whitespace!.id, a); whitespace = linesLayout.getWhitespaceAtVerticalOffset(61); - assert.equal(whitespace!.id, a); + assert.strictEqual(whitespace!.id, a); whitespace = linesLayout.getWhitespaceAtVerticalOffset(159); - assert.equal(whitespace!.id, a); + assert.strictEqual(whitespace!.id, a); whitespace = linesLayout.getWhitespaceAtVerticalOffset(160); - assert.equal(whitespace, null); + assert.strictEqual(whitespace, null); whitespace = linesLayout.getWhitespaceAtVerticalOffset(161); - assert.equal(whitespace, null); + assert.strictEqual(whitespace, null); whitespace = linesLayout.getWhitespaceAtVerticalOffset(169); - assert.equal(whitespace, null); + assert.strictEqual(whitespace, null); whitespace = linesLayout.getWhitespaceAtVerticalOffset(170); - assert.equal(whitespace!.id, b); + assert.strictEqual(whitespace!.id, b); whitespace = linesLayout.getWhitespaceAtVerticalOffset(171); - assert.equal(whitespace!.id, b); + assert.strictEqual(whitespace!.id, b); whitespace = linesLayout.getWhitespaceAtVerticalOffset(219); - assert.equal(whitespace!.id, b); + assert.strictEqual(whitespace!.id, b); whitespace = linesLayout.getWhitespaceAtVerticalOffset(220); - assert.equal(whitespace, null); + assert.strictEqual(whitespace, null); }); test('LinesLayout', () => { @@ -714,230 +714,230 @@ suite('Editor ViewLayout - LinesLayout', () => { // Insert a whitespace after line number 2, of height 10 const a = insertWhitespace(linesLayout, 2, 0, 10, 0); // whitespaces: a(2, 10) - assert.equal(linesLayout.getWhitespacesCount(), 1); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 10); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 10); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 10); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 10); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 10); + assert.strictEqual(linesLayout.getWhitespacesCount(), 1); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 10); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 10); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 10); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 10); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 10); // Insert a whitespace again after line number 2, of height 20 let b = insertWhitespace(linesLayout, 2, 0, 20, 0); // whitespaces: a(2, 10), b(2, 20) - assert.equal(linesLayout.getWhitespacesCount(), 2); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 10); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 10); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 30); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 30); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 30); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 30); + assert.strictEqual(linesLayout.getWhitespacesCount(), 2); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 10); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 10); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 30); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 30); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 30); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 30); // Change last inserted whitespace height to 30 changeOneWhitespace(linesLayout, b, 2, 30); // whitespaces: a(2, 10), b(2, 30) - assert.equal(linesLayout.getWhitespacesCount(), 2); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 10); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 30); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 10); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 40); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 40); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 40); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 40); + assert.strictEqual(linesLayout.getWhitespacesCount(), 2); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 10); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 30); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 10); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 40); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 40); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 40); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 40); // Remove last inserted whitespace removeWhitespace(linesLayout, b); // whitespaces: a(2, 10) - assert.equal(linesLayout.getWhitespacesCount(), 1); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 10); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 10); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 10); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 10); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 10); + assert.strictEqual(linesLayout.getWhitespacesCount(), 1); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 10); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 10); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 10); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 10); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 10); // Add a whitespace before the first line of height 50 b = insertWhitespace(linesLayout, 0, 0, 50, 0); // whitespaces: b(0, 50), a(2, 10) - assert.equal(linesLayout.getWhitespacesCount(), 2); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 50); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 10); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 50); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 60); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 60); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 60); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 60); + assert.strictEqual(linesLayout.getWhitespacesCount(), 2); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 50); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 10); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 50); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 60); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 60); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 60); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 60); // Add a whitespace after line 4 of height 20 insertWhitespace(linesLayout, 4, 0, 20, 0); // whitespaces: b(0, 50), a(2, 10), c(4, 20) - assert.equal(linesLayout.getWhitespacesCount(), 3); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 50); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 10); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 4); - assert.equal(linesLayout.getHeightForWhitespaceIndex(2), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 50); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 60); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(2), 80); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 80); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 60); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 60); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 80); + assert.strictEqual(linesLayout.getWhitespacesCount(), 3); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 50); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 10); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 4); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(2), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 50); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 60); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(2), 80); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 80); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 60); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 60); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 80); // Add a whitespace after line 3 of height 30 insertWhitespace(linesLayout, 3, 0, 30, 0); // whitespaces: b(0, 50), a(2, 10), d(3, 30), c(4, 20) - assert.equal(linesLayout.getWhitespacesCount(), 4); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 50); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 10); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(2), 30); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(3), 4); - assert.equal(linesLayout.getHeightForWhitespaceIndex(3), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 50); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 60); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(2), 90); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(3), 110); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 110); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 60); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 90); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 110); + assert.strictEqual(linesLayout.getWhitespacesCount(), 4); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 50); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 10); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(2), 30); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(3), 4); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(3), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 50); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 60); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(2), 90); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(3), 110); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 110); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 60); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 90); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 110); // Change whitespace after line 2 to height of 100 changeOneWhitespace(linesLayout, a, 2, 100); // whitespaces: b(0, 50), a(2, 100), d(3, 30), c(4, 20) - assert.equal(linesLayout.getWhitespacesCount(), 4); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 50); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 100); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(2), 30); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(3), 4); - assert.equal(linesLayout.getHeightForWhitespaceIndex(3), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 50); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 150); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(2), 180); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(3), 200); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 200); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 150); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 180); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 200); + assert.strictEqual(linesLayout.getWhitespacesCount(), 4); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 50); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 100); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(2), 30); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(3), 4); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(3), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 50); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 150); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(2), 180); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(3), 200); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 200); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 150); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 180); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 200); // Remove whitespace after line 2 removeWhitespace(linesLayout, a); // whitespaces: b(0, 50), d(3, 30), c(4, 20) - assert.equal(linesLayout.getWhitespacesCount(), 3); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 50); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 30); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 4); - assert.equal(linesLayout.getHeightForWhitespaceIndex(2), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 50); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 80); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(2), 100); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 100); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 80); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 100); + assert.strictEqual(linesLayout.getWhitespacesCount(), 3); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 50); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 30); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 4); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(2), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 50); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 80); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(2), 100); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 100); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 80); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 100); // Remove whitespace before line 1 removeWhitespace(linesLayout, b); // whitespaces: d(3, 30), c(4, 20) - assert.equal(linesLayout.getWhitespacesCount(), 2); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 30); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 4); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 30); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 50); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 30); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); + assert.strictEqual(linesLayout.getWhitespacesCount(), 2); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 30); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 4); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 30); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 50); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 30); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); // Delete line 1 linesLayout.onLinesDeleted(1, 1); // whitespaces: d(2, 30), c(3, 20) - assert.equal(linesLayout.getWhitespacesCount(), 2); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 30); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 30); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 50); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 30); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); + assert.strictEqual(linesLayout.getWhitespacesCount(), 2); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 30); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 30); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 50); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 30); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); // Insert a line before line 1 linesLayout.onLinesInserted(1, 1); // whitespaces: d(3, 30), c(4, 20) - assert.equal(linesLayout.getWhitespacesCount(), 2); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 30); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 4); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 30); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 50); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 30); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); + assert.strictEqual(linesLayout.getWhitespacesCount(), 2); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 30); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 4); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 30); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 50); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 30); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); // Delete line 4 linesLayout.onLinesDeleted(4, 4); // whitespaces: d(3, 30), c(3, 20) - assert.equal(linesLayout.getWhitespacesCount(), 2); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 30); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 30); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 50); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); + assert.strictEqual(linesLayout.getWhitespacesCount(), 2); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 30); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 30); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 50); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); }); test('LinesLayout findInsertionIndex', () => { @@ -949,114 +949,114 @@ suite('Editor ViewLayout - LinesLayout', () => { let arr: EditorWhitespace[]; arr = makeInternalWhitespace([]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 0); arr = makeInternalWhitespace([1]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); arr = makeInternalWhitespace([1, 3]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); arr = makeInternalWhitespace([1, 3, 5]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 6, 0), 3); arr = makeInternalWhitespace([1, 3, 5], 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 5, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 6, 0), 3); arr = makeInternalWhitespace([1, 3, 5, 7]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 7, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 8, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 7, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 8, 0), 4); arr = makeInternalWhitespace([1, 3, 5, 7, 9]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 7, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 8, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 9, 0), 5); - assert.equal(LinesLayout.findInsertionIndex(arr, 10, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 7, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 8, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 9, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 10, 0), 5); arr = makeInternalWhitespace([1, 3, 5, 7, 9, 11]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 7, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 8, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 9, 0), 5); - assert.equal(LinesLayout.findInsertionIndex(arr, 10, 0), 5); - assert.equal(LinesLayout.findInsertionIndex(arr, 11, 0), 6); - assert.equal(LinesLayout.findInsertionIndex(arr, 12, 0), 6); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 7, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 8, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 9, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 10, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 11, 0), 6); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 12, 0), 6); arr = makeInternalWhitespace([1, 3, 5, 7, 9, 11, 13]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 7, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 8, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 9, 0), 5); - assert.equal(LinesLayout.findInsertionIndex(arr, 10, 0), 5); - assert.equal(LinesLayout.findInsertionIndex(arr, 11, 0), 6); - assert.equal(LinesLayout.findInsertionIndex(arr, 12, 0), 6); - assert.equal(LinesLayout.findInsertionIndex(arr, 13, 0), 7); - assert.equal(LinesLayout.findInsertionIndex(arr, 14, 0), 7); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 7, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 8, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 9, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 10, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 11, 0), 6); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 12, 0), 6); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 13, 0), 7); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 14, 0), 7); arr = makeInternalWhitespace([1, 3, 5, 7, 9, 11, 13, 15]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 7, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 8, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 9, 0), 5); - assert.equal(LinesLayout.findInsertionIndex(arr, 10, 0), 5); - assert.equal(LinesLayout.findInsertionIndex(arr, 11, 0), 6); - assert.equal(LinesLayout.findInsertionIndex(arr, 12, 0), 6); - assert.equal(LinesLayout.findInsertionIndex(arr, 13, 0), 7); - assert.equal(LinesLayout.findInsertionIndex(arr, 14, 0), 7); - assert.equal(LinesLayout.findInsertionIndex(arr, 15, 0), 8); - assert.equal(LinesLayout.findInsertionIndex(arr, 16, 0), 8); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 7, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 8, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 9, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 10, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 11, 0), 6); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 12, 0), 6); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 13, 0), 7); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 14, 0), 7); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 15, 0), 8); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 16, 0), 8); }); test('LinesLayout changeWhitespaceAfterLineNumber & getFirstWhitespaceIndexAfterLineNumber', () => { @@ -1066,121 +1066,121 @@ suite('Editor ViewLayout - LinesLayout', () => { const b = insertWhitespace(linesLayout, 7, 0, 1, 0); const c = insertWhitespace(linesLayout, 3, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(1), c); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), c); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 1); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 1); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 1); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 1); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- // Do not really move a changeOneWhitespace(linesLayout, a, 1, 1); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 1 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 1); - assert.equal(linesLayout.getIdForWhitespaceIndex(1), c); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 1 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 1); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), c); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 1); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 1); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- // Do not really move a changeOneWhitespace(linesLayout, a, 2, 1); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 2 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(linesLayout.getIdForWhitespaceIndex(1), c); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 2 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), c); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // a - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // a + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- // Change a to conflict with c => a gets placed after c changeOneWhitespace(linesLayout, a, 3, 1); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), c); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(1), a); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), c); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), a); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 0); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 0); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- // Make a no-op changeOneWhitespace(linesLayout, c, 3, 1); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), c); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(1), a); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), c); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), a); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 0); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 0); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- // Conflict c with b => c gets placed after b changeOneWhitespace(linesLayout, c, 7, 1); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(1), b); // 7 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 7); - assert.equal(linesLayout.getIdForWhitespaceIndex(2), c); // 7 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), b); // 7 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 7); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), c); // 7 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // a - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 0); // a - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 1); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 1); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 1); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 1); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // a + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 0); // a + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 1); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 1); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 1); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 1); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- }); test('LinesLayout Bug', () => { @@ -1189,53 +1189,53 @@ suite('Editor ViewLayout - LinesLayout', () => { const a = insertWhitespace(linesLayout, 0, 0, 1, 0); const b = insertWhitespace(linesLayout, 7, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(1), b); // 7 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), b); // 7 const c = insertWhitespace(linesLayout, 3, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(1), c); // 3 - assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), c); // 3 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), b); // 7 const d = insertWhitespace(linesLayout, 2, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(1), d); // 2 - assert.equal(linesLayout.getIdForWhitespaceIndex(2), c); // 3 - assert.equal(linesLayout.getIdForWhitespaceIndex(3), b); // 7 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), d); // 2 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), c); // 3 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(3), b); // 7 const e = insertWhitespace(linesLayout, 8, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(1), d); // 2 - assert.equal(linesLayout.getIdForWhitespaceIndex(2), c); // 3 - assert.equal(linesLayout.getIdForWhitespaceIndex(3), b); // 7 - assert.equal(linesLayout.getIdForWhitespaceIndex(4), e); // 8 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), d); // 2 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), c); // 3 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(3), b); // 7 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(4), e); // 8 const f = insertWhitespace(linesLayout, 11, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(1), d); // 2 - assert.equal(linesLayout.getIdForWhitespaceIndex(2), c); // 3 - assert.equal(linesLayout.getIdForWhitespaceIndex(3), b); // 7 - assert.equal(linesLayout.getIdForWhitespaceIndex(4), e); // 8 - assert.equal(linesLayout.getIdForWhitespaceIndex(5), f); // 11 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), d); // 2 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), c); // 3 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(3), b); // 7 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(4), e); // 8 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(5), f); // 11 const g = insertWhitespace(linesLayout, 10, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(1), d); // 2 - assert.equal(linesLayout.getIdForWhitespaceIndex(2), c); // 3 - assert.equal(linesLayout.getIdForWhitespaceIndex(3), b); // 7 - assert.equal(linesLayout.getIdForWhitespaceIndex(4), e); // 8 - assert.equal(linesLayout.getIdForWhitespaceIndex(5), g); // 10 - assert.equal(linesLayout.getIdForWhitespaceIndex(6), f); // 11 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), d); // 2 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), c); // 3 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(3), b); // 7 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(4), e); // 8 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(5), g); // 10 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(6), f); // 11 const h = insertWhitespace(linesLayout, 0, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(1), h); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(2), d); // 2 - assert.equal(linesLayout.getIdForWhitespaceIndex(3), c); // 3 - assert.equal(linesLayout.getIdForWhitespaceIndex(4), b); // 7 - assert.equal(linesLayout.getIdForWhitespaceIndex(5), e); // 8 - assert.equal(linesLayout.getIdForWhitespaceIndex(6), g); // 10 - assert.equal(linesLayout.getIdForWhitespaceIndex(7), f); // 11 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), h); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), d); // 2 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(3), c); // 3 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(4), b); // 7 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(5), e); // 8 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(6), g); // 10 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(7), f); // 11 }); }); diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index 46f36901..672a4a86 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -48,7 +48,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expected + ''); + assert.strictEqual(_actual.html, '' + expected + ''); assertCharacterMapping(_actual.characterMapping, expectedCharOffsetInPart, expectedPartLengts); } @@ -101,7 +101,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expected + ''); + assert.strictEqual(_actual.html, '' + expected + ''); assertCharacterMapping(_actual.characterMapping, expectedCharOffsetInPart, expectedPartLengts); } @@ -167,7 +167,7 @@ suite('viewLineRenderer.renderLine', () => { '' ].join(''); - assert.equal(_actual.html, '' + expectedOutput + ''); + assert.strictEqual(_actual.html, '' + expectedOutput + ''); assertCharacterMapping(_actual.characterMapping, [ [0], @@ -252,7 +252,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expectedOutput + ''); + assert.strictEqual(_actual.html, '' + expectedOutput + ''); assertCharacterMapping(_actual.characterMapping, expectedOffsetsArr, [4, 4, 6, 1, 5, 1, 4, 1, 1, 1, 3, 15, 2, 3]); }); @@ -318,7 +318,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expectedOutput + ''); + assert.strictEqual(_actual.html, '' + expectedOutput + ''); assertCharacterMapping(_actual.characterMapping, expectedOffsetsArr, [12, 12, 24, 1, 21, 2, 1, 20, 1, 1]); }); @@ -384,7 +384,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expectedOutput + ''); + assert.strictEqual(_actual.html, '' + expectedOutput + ''); assertCharacterMapping(_actual.characterMapping, expectedOffsetsArr, [12, 12, 24, 1, 21, 2, 1, 20, 1, 1]); }); @@ -444,7 +444,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(actual.html, '' + expectedOutput + ''); + assert.strictEqual(actual.html, '' + expectedOutput + ''); assertCharacterMapping2(actual.characterMapping, expectedCharacterMapping); }); @@ -487,8 +487,8 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expectedOutput + ''); - assert.equal(_actual.containsRTL, true); + assert.strictEqual(_actual.html, '' + expectedOutput + ''); + assert.strictEqual(_actual.containsRTL, true); }); test('issue #6885: Splits large tokens', () => { @@ -520,7 +520,7 @@ suite('viewLineRenderer.renderLine', () => { false, null )); - assert.equal(actual.html, '' + expectedOutput.join('') + '', message); + assert.strictEqual(actual.html, '' + expectedOutput.join('') + '', message); } // A token with 49 chars @@ -624,7 +624,7 @@ suite('viewLineRenderer.renderLine', () => { true, null )); - assert.equal(actual.html, '' + expectedOutput.join('') + '', message); + assert.strictEqual(actual.html, '' + expectedOutput.join('') + '', message); } // A token with 101 chars @@ -669,7 +669,7 @@ suite('viewLineRenderer.renderLine', () => { let expectedOutput = [ 'a𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷', ]; - assert.equal(actual.html, '' + expectedOutput.join('') + ''); + assert.strictEqual(actual.html, '' + expectedOutput.join('') + ''); }); test('issue #6885: Does not split large tokens in RTL text', () => { @@ -699,8 +699,8 @@ suite('viewLineRenderer.renderLine', () => { false, null )); - assert.equal(actual.html, '' + expectedOutput.join('') + ''); - assert.equal(actual.containsRTL, true); + assert.strictEqual(actual.html, '' + expectedOutput.join('') + ''); + assert.strictEqual(actual.containsRTL, true); }); test('issue #95685: Uses unicode replacement character for Paragraph Separator', () => { @@ -730,7 +730,7 @@ suite('viewLineRenderer.renderLine', () => { false, null )); - assert.equal(actual.html, '' + expectedOutput.join('') + ''); + assert.strictEqual(actual.html, '' + expectedOutput.join('') + ''); }); test('issue #19673: Monokai Theme bad-highlighting in line wrap', () => { @@ -780,7 +780,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expectedOutput + ''); + assert.strictEqual(_actual.html, '' + expectedOutput + ''); }); interface ICharMappingData { @@ -807,7 +807,7 @@ suite('viewLineRenderer.renderLine', () => { function assertCharacterMapping2(actual: CharacterMapping, expected: CharacterMapping): void { const _actual = decodeCharacterMapping(actual); const _expected = decodeCharacterMapping(expected); - assert.deepEqual(_actual, _expected); + assert.deepStrictEqual(_actual, _expected); } function assertCharacterMapping(actual: CharacterMapping, expectedCharPartOffsets: number[][], expectedPartLengths: number[]): void { @@ -830,7 +830,7 @@ suite('viewLineRenderer.renderLine', () => { for (let i = 0; i < tmp.length; i++) { actualCharOffset[i] = tmp[i]; } - assert.deepEqual(actualCharOffset, expectedCharAbsoluteOffset); + assert.deepStrictEqual(actualCharOffset, expectedCharAbsoluteOffset); } function assertCharPartOffsets(actual: CharacterMapping, expected: number[][]): void { @@ -844,7 +844,7 @@ suite('viewLineRenderer.renderLine', () => { let actualPartIndex = CharacterMapping.getPartIndex(_actualPartData); let actualCharIndex = CharacterMapping.getCharIndex(_actualPartData); - assert.deepEqual( + assert.deepStrictEqual( { partIndex: actualPartIndex, charIndex: actualCharIndex }, { partIndex: partIndex, charIndex: charIndex }, `character mapping for offset ${charOffset}` @@ -853,7 +853,7 @@ suite('viewLineRenderer.renderLine', () => { // here let actualOffset = actual.partDataToCharOffset(partIndex, part[part.length - 1] + 1, charIndex); - assert.equal( + assert.strictEqual( actualOffset, charOffset, `character mapping for part ${partIndex}, ${charIndex}` @@ -863,7 +863,7 @@ suite('viewLineRenderer.renderLine', () => { } } - assert.equal(actual.length, charOffset); + assert.strictEqual(actual.length, charOffset); } }); @@ -892,7 +892,7 @@ suite('viewLineRenderer.renderLine 2', () => { selections )); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); } test('issue #18616: Inline decorations ending at the text length are no longer rendered', () => { @@ -927,7 +927,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #19207: Link in Monokai is not rendered correctly', () => { @@ -976,7 +976,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('createLineParts simple', () => { @@ -1477,7 +1477,7 @@ suite('viewLineRenderer.renderLine 2', () => { // bb--------- // -cccccc---- - assert.deepEqual(actual.html, [ + assert.deepStrictEqual(actual.html, [ '', 'H', 'e', @@ -1522,7 +1522,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #32436: Non-monospace font + visible whitespace + After decorator causes line to "jump"', () => { @@ -1559,7 +1559,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #30133: Empty lines don\'t render inline decorations', () => { @@ -1594,7 +1594,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #37208: Collapsing bullet point containing emoji in Markdown document results in [??] character', () => { @@ -1628,7 +1628,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #37401 #40127: Allow both before and after decorations on empty line', () => { @@ -1665,7 +1665,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #38935: GitLens end-of-line blame no longer rendering', () => { @@ -1702,7 +1702,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #22832: Consider fullwidth characters when rendering tabs', () => { @@ -1735,7 +1735,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #22832: Consider fullwidth characters when rendering tabs (render whitespace)', () => { @@ -1774,7 +1774,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #22352: COMBINING ACUTE ACCENT (U+0301)', () => { @@ -1807,7 +1807,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #22352: Partially Broken Complex Script Rendering of Tamil', () => { @@ -1842,7 +1842,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #42700: Hindi characters are not being rendered properly', () => { @@ -1877,7 +1877,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #38123: editor.renderWhitespace: "boundary" renders whitespace at line wrap point when line is wrapped', () => { @@ -1909,7 +1909,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #33525: Long line with ligatures takes a long time to paint decorations', () => { @@ -1945,7 +1945,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #33525: Long line with ligatures takes a long time to paint decorations - not possible', () => { @@ -1977,7 +1977,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #91936: Semantic token color highlighting fails on line with selected text', () => { @@ -2062,7 +2062,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); @@ -2092,7 +2092,7 @@ suite('viewLineRenderer.renderLine 2', () => { return (partIndex: number, partLength: number, offset: number, expected: number) => { let charOffset = renderLineOutput.characterMapping.partDataToCharOffset(partIndex, partLength, offset); let actual = charOffset + 1; - assert.equal(actual, expected, 'getColumnOfLinePartOffset for ' + partIndex + ' @ ' + offset); + assert.strictEqual(actual, expected, 'getColumnOfLinePartOffset for ' + partIndex + ' @ ' + offset); }; } diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index 670f4240..eec0e2b2 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -47,6 +47,7 @@ function toAnnotatedText(text: string, lineBreakData: LineBreakData | null): str 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, + pixelRatio: 1, fontFamily: 'testFontFamily', fontWeight: 'normal', fontSize: 14, @@ -55,7 +56,7 @@ function getLineBreakData(factory: ILineBreaksComputerFactory, tabSize: number, letterSpacing: 0, isMonospace: true, typicalHalfwidthCharacterWidth: 7, - typicalFullwidthCharacterWidth: 14, + typicalFullwidthCharacterWidth: 7 * columnsForFullWidthChar, canUseHalfwidthRightwardsArrow: true, spaceWidth: 7, middotWidth: 7, @@ -74,7 +75,7 @@ function assertLineBreaks(factory: ILineBreaksComputerFactory, tabSize: number, const lineBreakData = getLineBreakData(factory, tabSize, breakAfter, 2, wrappingIndent, text, null); const actualAnnotatedText = toAnnotatedText(text, lineBreakData); - assert.equal(actualAnnotatedText, annotatedText); + assert.strictEqual(actualAnnotatedText, annotatedText); return lineBreakData; } @@ -122,28 +123,41 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { 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 { + function assertLineBreakDataEqual(a: LineBreakData | null, b: LineBreakData | null): void { + if (!a || !b) { + assert.deepStrictEqual(a, b); + return; + } + assert.deepStrictEqual(a.breakOffsets, b.breakOffsets); + assert.deepStrictEqual(a.wrappedTextIndentLength, b.wrappedTextIndentLength); + for (let i = 0; i < a.breakOffsetsVisibleColumn.length; i++) { + const diff = a.breakOffsetsVisibleColumn[i] - b.breakOffsetsVisibleColumn[i]; + assert.ok(diff < 0.001); + } + } + + function assertIncrementalLineBreaks(factory: ILineBreaksComputerFactory, text: string, tabSize: number, breakAfter1: number, annotatedText1: string, breakAfter2: number, annotatedText2: string, wrappingIndent = WrappingIndent.None, columnsForFullWidthChar: number = 2): void { // sanity check the test - assert.equal(text, parseAnnotatedText(annotatedText1).text); - assert.equal(text, parseAnnotatedText(annotatedText2).text); + assert.strictEqual(text, parseAnnotatedText(annotatedText1).text); + assert.strictEqual(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); + const directLineBreakData1 = getLineBreakData(factory, tabSize, breakAfter1, columnsForFullWidthChar, wrappingIndent, text, null); + assert.strictEqual(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); + const directLineBreakData2 = getLineBreakData(factory, tabSize, breakAfter2, columnsForFullWidthChar, wrappingIndent, text, null); + assert.strictEqual(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); + const lineBreakData2from1 = getLineBreakData(factory, tabSize, breakAfter2, columnsForFullWidthChar, wrappingIndent, text, directLineBreakData1); + assert.strictEqual(toAnnotatedText(text, lineBreakData2from1), annotatedText2); + assertLineBreakDataEqual(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); + const lineBreakData1from2 = getLineBreakData(factory, tabSize, breakAfter1, columnsForFullWidthChar, wrappingIndent, text, directLineBreakData2); + assert.strictEqual(toAnnotatedText(text, lineBreakData1from2), annotatedText1); + assertLineBreakDataEqual(lineBreakData1from2, directLineBreakData1); } test('MonospaceLineBreaksComputer incremental 1', () => { @@ -216,6 +230,19 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { ); }); + test('issue #110392: Occasional crash when resize with panel on the right', () => { + const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue); + assertIncrementalLineBreaks( + factory, + '你好 **hello** **hello** **hello-world** hey there!', + 4, + 15, '你好 **hello** |**hello** |**hello-world**| hey there!', + 1, '你|好| |*|*|h|e|l|l|o|*|*| |*|*|h|e|l|l|o|*|*| |*|*|h|e|l|l|o|-|w|o|r|l|d|*|*| |h|e|y| |t|h|e|r|e|!', + WrappingIndent.Same, + 1.6605405405405405 + ); + }); + test('MonospaceLineBreaksComputer - CJK and Kinsoku Shori', () => { let factory = new MonospaceLineBreaksComputerFactory('(', '\t)'); assertLineBreaks(factory, 4, 5, 'aa \u5b89|\u5b89'); @@ -239,7 +266,7 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { 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); + assert.strictEqual(mapper!.wrappedTextIndentLength, ' '.length); }); test('issue #75494: surrogate pairs', () => { @@ -260,11 +287,16 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { 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); + assert.strictEqual(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); }); + + test('issue #112382: Word wrap doesn\'t work well with control characters', () => { + const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue); + assertLineBreaks(factory, 4, 6, '\x06\x06\x06|\x06\x06\x06', 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 65cc26ef..80df1a94 100644 --- a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts @@ -22,158 +22,158 @@ suite('Editor ViewModel - PrefixSumComputer', () => { let indexOfResult: PrefixSumIndexOfResult; let psc = new PrefixSumComputer(toUint32Array([1, 1, 2, 1, 3])); - assert.equal(psc.getTotalValue(), 8); - assert.equal(psc.getAccumulatedValue(-1), 0); - assert.equal(psc.getAccumulatedValue(0), 1); - assert.equal(psc.getAccumulatedValue(1), 2); - assert.equal(psc.getAccumulatedValue(2), 4); - assert.equal(psc.getAccumulatedValue(3), 5); - assert.equal(psc.getAccumulatedValue(4), 8); + assert.strictEqual(psc.getTotalValue(), 8); + assert.strictEqual(psc.getAccumulatedValue(-1), 0); + assert.strictEqual(psc.getAccumulatedValue(0), 1); + assert.strictEqual(psc.getAccumulatedValue(1), 2); + assert.strictEqual(psc.getAccumulatedValue(2), 4); + assert.strictEqual(psc.getAccumulatedValue(3), 5); + assert.strictEqual(psc.getAccumulatedValue(4), 8); indexOfResult = psc.getIndexOf(0); - assert.equal(indexOfResult.index, 0); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 0); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(1); - assert.equal(indexOfResult.index, 1); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 1); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(2); - assert.equal(indexOfResult.index, 2); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 2); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(3); - assert.equal(indexOfResult.index, 2); - assert.equal(indexOfResult.remainder, 1); + assert.strictEqual(indexOfResult.index, 2); + assert.strictEqual(indexOfResult.remainder, 1); indexOfResult = psc.getIndexOf(4); - assert.equal(indexOfResult.index, 3); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 3); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(5); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(6); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 1); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 1); indexOfResult = psc.getIndexOf(7); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 2); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 2); indexOfResult = psc.getIndexOf(8); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 3); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 3); // [1, 2, 2, 1, 3] psc.changeValue(1, 2); - assert.equal(psc.getTotalValue(), 9); - assert.equal(psc.getAccumulatedValue(0), 1); - assert.equal(psc.getAccumulatedValue(1), 3); - assert.equal(psc.getAccumulatedValue(2), 5); - assert.equal(psc.getAccumulatedValue(3), 6); - assert.equal(psc.getAccumulatedValue(4), 9); + assert.strictEqual(psc.getTotalValue(), 9); + assert.strictEqual(psc.getAccumulatedValue(0), 1); + assert.strictEqual(psc.getAccumulatedValue(1), 3); + assert.strictEqual(psc.getAccumulatedValue(2), 5); + assert.strictEqual(psc.getAccumulatedValue(3), 6); + assert.strictEqual(psc.getAccumulatedValue(4), 9); // [1, 0, 2, 1, 3] psc.changeValue(1, 0); - assert.equal(psc.getTotalValue(), 7); - assert.equal(psc.getAccumulatedValue(0), 1); - assert.equal(psc.getAccumulatedValue(1), 1); - assert.equal(psc.getAccumulatedValue(2), 3); - assert.equal(psc.getAccumulatedValue(3), 4); - assert.equal(psc.getAccumulatedValue(4), 7); + assert.strictEqual(psc.getTotalValue(), 7); + assert.strictEqual(psc.getAccumulatedValue(0), 1); + assert.strictEqual(psc.getAccumulatedValue(1), 1); + assert.strictEqual(psc.getAccumulatedValue(2), 3); + assert.strictEqual(psc.getAccumulatedValue(3), 4); + assert.strictEqual(psc.getAccumulatedValue(4), 7); indexOfResult = psc.getIndexOf(0); - assert.equal(indexOfResult.index, 0); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 0); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(1); - assert.equal(indexOfResult.index, 2); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 2); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(2); - assert.equal(indexOfResult.index, 2); - assert.equal(indexOfResult.remainder, 1); + assert.strictEqual(indexOfResult.index, 2); + assert.strictEqual(indexOfResult.remainder, 1); indexOfResult = psc.getIndexOf(3); - assert.equal(indexOfResult.index, 3); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 3); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(4); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(5); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 1); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 1); indexOfResult = psc.getIndexOf(6); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 2); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 2); indexOfResult = psc.getIndexOf(7); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 3); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 3); // [1, 0, 0, 1, 3] psc.changeValue(2, 0); - assert.equal(psc.getTotalValue(), 5); - assert.equal(psc.getAccumulatedValue(0), 1); - assert.equal(psc.getAccumulatedValue(1), 1); - assert.equal(psc.getAccumulatedValue(2), 1); - assert.equal(psc.getAccumulatedValue(3), 2); - assert.equal(psc.getAccumulatedValue(4), 5); + assert.strictEqual(psc.getTotalValue(), 5); + assert.strictEqual(psc.getAccumulatedValue(0), 1); + assert.strictEqual(psc.getAccumulatedValue(1), 1); + assert.strictEqual(psc.getAccumulatedValue(2), 1); + assert.strictEqual(psc.getAccumulatedValue(3), 2); + assert.strictEqual(psc.getAccumulatedValue(4), 5); indexOfResult = psc.getIndexOf(0); - assert.equal(indexOfResult.index, 0); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 0); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(1); - assert.equal(indexOfResult.index, 3); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 3); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(2); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(3); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 1); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 1); indexOfResult = psc.getIndexOf(4); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 2); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 2); indexOfResult = psc.getIndexOf(5); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 3); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 3); // [1, 0, 0, 0, 3] psc.changeValue(3, 0); - assert.equal(psc.getTotalValue(), 4); - assert.equal(psc.getAccumulatedValue(0), 1); - assert.equal(psc.getAccumulatedValue(1), 1); - assert.equal(psc.getAccumulatedValue(2), 1); - assert.equal(psc.getAccumulatedValue(3), 1); - assert.equal(psc.getAccumulatedValue(4), 4); + assert.strictEqual(psc.getTotalValue(), 4); + assert.strictEqual(psc.getAccumulatedValue(0), 1); + assert.strictEqual(psc.getAccumulatedValue(1), 1); + assert.strictEqual(psc.getAccumulatedValue(2), 1); + assert.strictEqual(psc.getAccumulatedValue(3), 1); + assert.strictEqual(psc.getAccumulatedValue(4), 4); indexOfResult = psc.getIndexOf(0); - assert.equal(indexOfResult.index, 0); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 0); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(1); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(2); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 1); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 1); indexOfResult = psc.getIndexOf(3); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 2); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 2); indexOfResult = psc.getIndexOf(4); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 3); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 3); // [1, 1, 0, 1, 1] psc.changeValue(1, 1); psc.changeValue(3, 1); psc.changeValue(4, 1); - assert.equal(psc.getTotalValue(), 4); - assert.equal(psc.getAccumulatedValue(0), 1); - assert.equal(psc.getAccumulatedValue(1), 2); - assert.equal(psc.getAccumulatedValue(2), 2); - assert.equal(psc.getAccumulatedValue(3), 3); - assert.equal(psc.getAccumulatedValue(4), 4); + assert.strictEqual(psc.getTotalValue(), 4); + assert.strictEqual(psc.getAccumulatedValue(0), 1); + assert.strictEqual(psc.getAccumulatedValue(1), 2); + assert.strictEqual(psc.getAccumulatedValue(2), 2); + assert.strictEqual(psc.getAccumulatedValue(3), 3); + assert.strictEqual(psc.getAccumulatedValue(4), 4); indexOfResult = psc.getIndexOf(0); - assert.equal(indexOfResult.index, 0); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 0); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(1); - assert.equal(indexOfResult.index, 1); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 1); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(2); - assert.equal(indexOfResult.index, 3); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 3); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(3); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(4); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 1); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 1); }); }); diff --git a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts index 3156be0e..3f797895 100644 --- a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts +++ b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts @@ -25,42 +25,42 @@ suite('Editor ViewModel - SplitLinesCollection', () => { let model1 = createModel('My First LineMy Second LineAnd another one'); 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'); - 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), 15); - assert.equal(line1.getViewLineMaxColumn(model1, 1, 2), 16); + assert.strictEqual(line1.getViewLineCount(), 3); + assert.strictEqual(line1.getViewLineContent(model1, 1, 0), 'My First Line'); + assert.strictEqual(line1.getViewLineContent(model1, 1, 1), 'My Second Line'); + assert.strictEqual(line1.getViewLineContent(model1, 1, 2), 'And another one'); + assert.strictEqual(line1.getViewLineMaxColumn(model1, 1, 0), 14); + assert.strictEqual(line1.getViewLineMaxColumn(model1, 1, 1), 15); + assert.strictEqual(line1.getViewLineMaxColumn(model1, 1, 2), 16); for (let col = 1; col <= 14; col++) { - assert.equal(line1.getModelColumnOfViewPosition(0, col), col, 'getInputColumnOfOutputPosition(0, ' + col + ')'); + assert.strictEqual(line1.getModelColumnOfViewPosition(0, col), col, 'getInputColumnOfOutputPosition(0, ' + col + ')'); } for (let col = 1; col <= 15; col++) { - assert.equal(line1.getModelColumnOfViewPosition(1, col), 13 + col, 'getInputColumnOfOutputPosition(1, ' + col + ')'); + assert.strictEqual(line1.getModelColumnOfViewPosition(1, col), 13 + col, 'getInputColumnOfOutputPosition(1, ' + col + ')'); } for (let col = 1; col <= 16; col++) { - assert.equal(line1.getModelColumnOfViewPosition(2, col), 13 + 14 + col, 'getInputColumnOfOutputPosition(2, ' + col + ')'); + assert.strictEqual(line1.getModelColumnOfViewPosition(2, col), 13 + 14 + col, 'getInputColumnOfOutputPosition(2, ' + col + ')'); } for (let col = 1; col <= 13; col++) { - assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(0, col), 'getOutputPositionOfInputPosition(' + col + ')'); + assert.deepStrictEqual(line1.getViewPositionOfModelPosition(0, col), pos(0, col), 'getOutputPositionOfInputPosition(' + col + ')'); } for (let col = 1 + 13; col <= 14 + 13; col++) { - assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(1, col - 13), 'getOutputPositionOfInputPosition(' + col + ')'); + assert.deepStrictEqual(line1.getViewPositionOfModelPosition(0, col), pos(1, col - 13), 'getOutputPositionOfInputPosition(' + col + ')'); } for (let col = 1 + 13 + 14; col <= 15 + 14 + 13; col++) { - assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(2, col - 13 - 14), 'getOutputPositionOfInputPosition(' + col + ')'); + assert.deepStrictEqual(line1.getViewPositionOfModelPosition(0, col), pos(2, col - 13 - 14), 'getOutputPositionOfInputPosition(' + col + ')'); } model1 = createModel('My First LineMy Second LineAnd another one'); 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), ' 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), 19); - assert.equal(line1.getViewLineMaxColumn(model1, 1, 2), 20); + assert.strictEqual(line1.getViewLineCount(), 3); + assert.strictEqual(line1.getViewLineContent(model1, 1, 0), 'My First Line'); + assert.strictEqual(line1.getViewLineContent(model1, 1, 1), ' My Second Line'); + assert.strictEqual(line1.getViewLineContent(model1, 1, 2), ' And another one'); + assert.strictEqual(line1.getViewLineMaxColumn(model1, 1, 0), 14); + assert.strictEqual(line1.getViewLineMaxColumn(model1, 1, 1), 19); + assert.strictEqual(line1.getViewLineMaxColumn(model1, 1, 2), 20); let actualViewColumnMapping: number[][] = []; for (let lineIndex = 0; lineIndex < line1.getViewLineCount(); lineIndex++) { @@ -70,20 +70,20 @@ suite('Editor ViewModel - SplitLinesCollection', () => { } actualViewColumnMapping.push(actualLineViewColumnMapping); } - assert.deepEqual(actualViewColumnMapping, [ + assert.deepStrictEqual(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), '6.getOutputPositionOfInputPosition(' + col + ')'); + assert.deepStrictEqual(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, 4 + col - 13), '7.getOutputPositionOfInputPosition(' + col + ')'); + assert.deepStrictEqual(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, 4 + col - 13 - 14), '8.getOutputPositionOfInputPosition(' + col + ')'); + assert.deepStrictEqual(line1.getViewPositionOfModelPosition(0, col), pos(2, 4 + col - 13 - 14), '8.getOutputPositionOfInputPosition(' + col + ')'); } }); @@ -136,65 +136,65 @@ suite('Editor ViewModel - SplitLinesCollection', () => { ].join('\n'); withSplitLinesCollection(text, (model, linesCollection) => { - assert.equal(linesCollection.getViewLineCount(), 6); + assert.strictEqual(linesCollection.getViewLineCount(), 6); // getOutputIndentGuide - assert.deepEqual(linesCollection.getViewLinesIndentGuides(-1, -1), [0]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(0, 0), [0]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(1, 1), [0]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(2, 2), [1]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(3, 3), [0]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(4, 4), [0]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(5, 5), [1]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(6, 6), [0]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(7, 7), [0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(-1, -1), [0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(0, 0), [0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(1, 1), [0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(2, 2), [1]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(3, 3), [0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(4, 4), [0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(5, 5), [1]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(6, 6), [0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(7, 7), [0]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(0, 7), [0, 1, 0, 0, 1, 0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(0, 7), [0, 1, 0, 0, 1, 0]); // getOutputLineContent - assert.equal(linesCollection.getViewLineContent(-1), 'int main() {'); - assert.equal(linesCollection.getViewLineContent(0), 'int main() {'); - assert.equal(linesCollection.getViewLineContent(1), 'int main() {'); - assert.equal(linesCollection.getViewLineContent(2), '\tprintf("Hello world!");'); - assert.equal(linesCollection.getViewLineContent(3), '}'); - assert.equal(linesCollection.getViewLineContent(4), 'int main() {'); - assert.equal(linesCollection.getViewLineContent(5), '\tprintf("Hello world!");'); - assert.equal(linesCollection.getViewLineContent(6), '}'); - assert.equal(linesCollection.getViewLineContent(7), '}'); + assert.strictEqual(linesCollection.getViewLineContent(-1), 'int main() {'); + assert.strictEqual(linesCollection.getViewLineContent(0), 'int main() {'); + assert.strictEqual(linesCollection.getViewLineContent(1), 'int main() {'); + assert.strictEqual(linesCollection.getViewLineContent(2), '\tprintf("Hello world!");'); + assert.strictEqual(linesCollection.getViewLineContent(3), '}'); + assert.strictEqual(linesCollection.getViewLineContent(4), 'int main() {'); + assert.strictEqual(linesCollection.getViewLineContent(5), '\tprintf("Hello world!");'); + assert.strictEqual(linesCollection.getViewLineContent(6), '}'); + assert.strictEqual(linesCollection.getViewLineContent(7), '}'); // getOutputLineMinColumn - assert.equal(linesCollection.getViewLineMinColumn(-1), 1); - assert.equal(linesCollection.getViewLineMinColumn(0), 1); - assert.equal(linesCollection.getViewLineMinColumn(1), 1); - assert.equal(linesCollection.getViewLineMinColumn(2), 1); - assert.equal(linesCollection.getViewLineMinColumn(3), 1); - assert.equal(linesCollection.getViewLineMinColumn(4), 1); - assert.equal(linesCollection.getViewLineMinColumn(5), 1); - assert.equal(linesCollection.getViewLineMinColumn(6), 1); - assert.equal(linesCollection.getViewLineMinColumn(7), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(-1), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(0), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(1), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(2), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(3), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(4), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(5), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(6), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(7), 1); // getOutputLineMaxColumn - assert.equal(linesCollection.getViewLineMaxColumn(-1), 13); - assert.equal(linesCollection.getViewLineMaxColumn(0), 13); - assert.equal(linesCollection.getViewLineMaxColumn(1), 13); - assert.equal(linesCollection.getViewLineMaxColumn(2), 25); - assert.equal(linesCollection.getViewLineMaxColumn(3), 2); - assert.equal(linesCollection.getViewLineMaxColumn(4), 13); - assert.equal(linesCollection.getViewLineMaxColumn(5), 25); - assert.equal(linesCollection.getViewLineMaxColumn(6), 2); - assert.equal(linesCollection.getViewLineMaxColumn(7), 2); + assert.strictEqual(linesCollection.getViewLineMaxColumn(-1), 13); + assert.strictEqual(linesCollection.getViewLineMaxColumn(0), 13); + assert.strictEqual(linesCollection.getViewLineMaxColumn(1), 13); + assert.strictEqual(linesCollection.getViewLineMaxColumn(2), 25); + assert.strictEqual(linesCollection.getViewLineMaxColumn(3), 2); + assert.strictEqual(linesCollection.getViewLineMaxColumn(4), 13); + assert.strictEqual(linesCollection.getViewLineMaxColumn(5), 25); + assert.strictEqual(linesCollection.getViewLineMaxColumn(6), 2); + assert.strictEqual(linesCollection.getViewLineMaxColumn(7), 2); // convertOutputPositionToInputPosition - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(-1, 1), new Position(1, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(0, 1), new Position(1, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(1, 1), new Position(1, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(2, 1), new Position(2, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(3, 1), new Position(3, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(4, 1), new Position(4, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(5, 1), new Position(5, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(6, 1), new Position(6, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(7, 1), new Position(6, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(8, 1), new Position(6, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(-1, 1), new Position(1, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(0, 1), new Position(1, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(1, 1), new Position(1, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(2, 1), new Position(2, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(3, 1), new Position(3, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(4, 1), new Position(4, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(5, 1), new Position(5, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(6, 1), new Position(6, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(7, 1), new Position(6, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(8, 1), new Position(6, 1)); }); }); @@ -216,7 +216,7 @@ suite('Editor ViewModel - SplitLinesCollection', () => { ]); let viewLineCount = linesCollection.getViewLineCount(); - assert.equal(viewLineCount, 1, 'getOutputLineCount()'); + assert.strictEqual(viewLineCount, 1, 'getOutputLineCount()'); let modelLineCount = model.getLineCount(); for (let lineNumber = 0; lineNumber <= modelLineCount + 1; lineNumber++) { @@ -244,7 +244,7 @@ suite('Editor ViewModel - SplitLinesCollection', () => { viewColumn = viewMaxColumn; } let validViewPosition = new Position(viewLineNumber, viewColumn); - assert.equal(viewPosition.toString(), validViewPosition.toString(), 'model->view for ' + lineNumber + ', ' + column); + assert.strictEqual(viewPosition.toString(), validViewPosition.toString(), 'model->view for ' + lineNumber + ', ' + column); } } @@ -254,7 +254,7 @@ suite('Editor ViewModel - SplitLinesCollection', () => { for (let column = lineMinColumn - 1; column <= lineMaxColumn + 1; column++) { let modelPosition = linesCollection.convertViewPositionToModelPosition(lineNumber, column); let validModelPosition = model.validatePosition(modelPosition); - assert.equal(modelPosition.toString(), validModelPosition.toString(), 'view->model for ' + lineNumber + ', ' + column); + assert.strictEqual(modelPosition.toString(), validModelPosition.toString(), 'view->model for ' + lineNumber + ', ' + column); } } }); @@ -333,7 +333,7 @@ suite('SplitLinesCollection', () => { const tokenizationSupport: modes.ITokenizationSupport = { getInitialState: () => NULL_STATE, tokenize: undefined!, - tokenize2: (line: string, state: modes.IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: modes.IState): TokenizationResult2 => { let tokens = _tokens[_lineIndex++]; let result = new Uint32Array(2 * tokens.length); @@ -374,7 +374,7 @@ suite('SplitLinesCollection', () => { value: _actual.getForeground(i) }; } - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } interface ITestMinimapLineRenderingData { @@ -391,16 +391,15 @@ suite('SplitLinesCollection', () => { } if (expected === null) { assert.ok(false); - return; } - assert.equal(actual.content, expected.content); - assert.equal(actual.minColumn, expected.minColumn); - assert.equal(actual.maxColumn, expected.maxColumn); + assert.strictEqual(actual.content, expected.content); + assert.strictEqual(actual.minColumn, expected.minColumn); + assert.strictEqual(actual.maxColumn, expected.maxColumn); assertViewLineTokens(actual.tokens, expected.tokens); } function assertMinimapLinesRenderingData(actual: ViewLineData[], expected: Array): void { - assert.equal(actual.length, expected.length); + assert.strictEqual(actual.length, expected.length); for (let i = 0; i < expected.length; i++) { assertMinimapLineRenderingData(actual[i], expected[i]); } @@ -429,15 +428,15 @@ suite('SplitLinesCollection', () => { test('getViewLinesData - no wrapping', () => { withSplitLinesCollection(model!, 'off', 0, (splitLinesCollection) => { - assert.equal(splitLinesCollection.getViewLineCount(), 8); - assert.equal(splitLinesCollection.modelPositionIsVisible(1, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(2, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(3, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(4, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(5, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(6, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(7, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(8, 1), true); + assert.strictEqual(splitLinesCollection.getViewLineCount(), 8); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(3, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(4, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(5, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(6, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(7, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(8, 1), true); let _expected: ITestMinimapLineRenderingData[] = [ { @@ -541,15 +540,15 @@ suite('SplitLinesCollection', () => { ]); splitLinesCollection.setHiddenAreas([new Range(2, 1, 4, 1)]); - assert.equal(splitLinesCollection.getViewLineCount(), 5); - assert.equal(splitLinesCollection.modelPositionIsVisible(1, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(2, 1), false); - assert.equal(splitLinesCollection.modelPositionIsVisible(3, 1), false); - assert.equal(splitLinesCollection.modelPositionIsVisible(4, 1), false); - assert.equal(splitLinesCollection.modelPositionIsVisible(5, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(6, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(7, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(8, 1), true); + assert.strictEqual(splitLinesCollection.getViewLineCount(), 5); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(3, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(4, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(5, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(6, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(7, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(8, 1), true); assertAllMinimapLinesRenderingData(splitLinesCollection, [ _expected[0], @@ -563,15 +562,15 @@ suite('SplitLinesCollection', () => { test('getViewLinesData - with wrapping', () => { withSplitLinesCollection(model!, 'wordWrapColumn', 30, (splitLinesCollection) => { - assert.equal(splitLinesCollection.getViewLineCount(), 12); - assert.equal(splitLinesCollection.modelPositionIsVisible(1, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(2, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(3, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(4, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(5, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(6, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(7, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(8, 1), true); + assert.strictEqual(splitLinesCollection.getViewLineCount(), 12); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(3, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(4, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(5, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(6, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(7, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(8, 1), true); let _expected: ITestMinimapLineRenderingData[] = [ { @@ -711,15 +710,15 @@ suite('SplitLinesCollection', () => { ]); splitLinesCollection.setHiddenAreas([new Range(2, 1, 4, 1)]); - assert.equal(splitLinesCollection.getViewLineCount(), 8); - assert.equal(splitLinesCollection.modelPositionIsVisible(1, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(2, 1), false); - assert.equal(splitLinesCollection.modelPositionIsVisible(3, 1), false); - assert.equal(splitLinesCollection.modelPositionIsVisible(4, 1), false); - assert.equal(splitLinesCollection.modelPositionIsVisible(5, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(6, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(7, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(8, 1), true); + assert.strictEqual(splitLinesCollection.getViewLineCount(), 8); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(3, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(4, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(5, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(6, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(7, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(8, 1), true); assertAllMinimapLinesRenderingData(splitLinesCollection, [ _expected[0], diff --git a/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts b/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts index cd990681..9b286277 100644 --- a/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts +++ b/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; -import { InlineDecorationType } from 'vs/editor/common/viewModel/viewModel'; +import { InlineDecoration, InlineDecorationType } from 'vs/editor/common/viewModel/viewModel'; import { testViewModel } from 'vs/editor/test/common/viewModel/testViewModel'; suite('ViewModelDecorations', () => { @@ -19,11 +19,11 @@ suite('ViewModelDecorations', () => { wordWrapColumn: 13 }; testViewModel(text, opts, (viewModel, model) => { - assert.equal(viewModel.getLineContent(1), 'hello world, '); - assert.equal(viewModel.getLineContent(2), 'this is a '); - assert.equal(viewModel.getLineContent(3), 'buffer that '); - assert.equal(viewModel.getLineContent(4), 'will be '); - assert.equal(viewModel.getLineContent(5), 'wrapped'); + assert.strictEqual(viewModel.getLineContent(1), 'hello world, '); + assert.strictEqual(viewModel.getLineContent(2), 'this is a '); + assert.strictEqual(viewModel.getLineContent(3), 'buffer that '); + assert.strictEqual(viewModel.getLineContent(4), 'will be '); + assert.strictEqual(viewModel.getLineContent(5), 'wrapped'); model.changeDecorations((accessor) => { let createOpts = (id: string) => { @@ -79,7 +79,7 @@ suite('ViewModelDecorations', () => { return dec.options.className; }).filter(Boolean); - assert.deepEqual(actualDecorations, [ + assert.deepStrictEqual(actualDecorations, [ 'dec1', 'dec2', 'dec3', @@ -102,112 +102,28 @@ suite('ViewModelDecorations', () => { ).inlineDecorations; // view line 2: (1,14 -> 1,24) - assert.deepEqual(inlineDecorations1, [ - { - range: new Range(1, 2, 2, 2), - inlineClassName: 'i-dec3', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 2, 2, 2), - inlineClassName: 'a-dec3', - type: InlineDecorationType.After - }, - { - range: new Range(1, 2, 3, 13), - inlineClassName: 'i-dec4', - type: InlineDecorationType.Regular - }, - { - range: new Range(1, 2, 5, 8), - inlineClassName: 'i-dec5', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 1, 2, 1), - inlineClassName: 'i-dec6', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 1, 2, 1), - inlineClassName: 'b-dec6', - type: InlineDecorationType.Before - }, - { - range: new Range(2, 1, 2, 1), - inlineClassName: 'a-dec6', - type: InlineDecorationType.After - }, - { - range: new Range(2, 1, 2, 3), - inlineClassName: 'i-dec7', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 1, 2, 1), - inlineClassName: 'b-dec7', - type: InlineDecorationType.Before - }, - { - range: new Range(2, 3, 2, 3), - inlineClassName: 'a-dec7', - type: InlineDecorationType.After - }, - { - range: new Range(2, 1, 3, 13), - inlineClassName: 'i-dec8', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 1, 2, 1), - inlineClassName: 'b-dec8', - type: InlineDecorationType.Before - }, - { - range: new Range(2, 1, 5, 8), - inlineClassName: 'i-dec9', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 1, 2, 1), - inlineClassName: 'b-dec9', - type: InlineDecorationType.Before - }, - { - range: new Range(2, 3, 2, 5), - inlineClassName: 'i-dec10', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 3, 2, 3), - inlineClassName: 'b-dec10', - type: InlineDecorationType.Before - }, - { - range: new Range(2, 5, 2, 5), - inlineClassName: 'a-dec10', - type: InlineDecorationType.After - }, - { - range: new Range(2, 3, 3, 13), - inlineClassName: 'i-dec11', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 3, 2, 3), - inlineClassName: 'b-dec11', - type: InlineDecorationType.Before - }, - { - range: new Range(2, 3, 5, 8), - inlineClassName: 'i-dec12', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 3, 2, 3), - inlineClassName: 'b-dec12', - type: InlineDecorationType.Before - }, + assert.deepStrictEqual(inlineDecorations1, [ + new InlineDecoration(new Range(1, 2, 2, 2), 'i-dec3', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 2, 2, 2), 'a-dec3', InlineDecorationType.After), + new InlineDecoration(new Range(1, 2, 3, 13), 'i-dec4', InlineDecorationType.Regular), + new InlineDecoration(new Range(1, 2, 5, 8), 'i-dec5', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 1, 2, 1), 'i-dec6', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 1, 2, 1), 'b-dec6', InlineDecorationType.Before), + new InlineDecoration(new Range(2, 1, 2, 1), 'a-dec6', InlineDecorationType.After), + new InlineDecoration(new Range(2, 1, 2, 3), 'i-dec7', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 1, 2, 1), 'b-dec7', InlineDecorationType.Before), + new InlineDecoration(new Range(2, 3, 2, 3), 'a-dec7', InlineDecorationType.After), + new InlineDecoration(new Range(2, 1, 3, 13), 'i-dec8', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 1, 2, 1), 'b-dec8', InlineDecorationType.Before), + new InlineDecoration(new Range(2, 1, 5, 8), 'i-dec9', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 1, 2, 1), 'b-dec9', InlineDecorationType.Before), + new InlineDecoration(new Range(2, 3, 2, 5), 'i-dec10', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 3, 2, 3), 'b-dec10', InlineDecorationType.Before), + new InlineDecoration(new Range(2, 5, 2, 5), 'a-dec10', InlineDecorationType.After), + new InlineDecoration(new Range(2, 3, 3, 13), 'i-dec11', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 3, 2, 3), 'b-dec11', InlineDecorationType.Before), + new InlineDecoration(new Range(2, 3, 5, 8), 'i-dec12', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 3, 2, 3), 'b-dec12', InlineDecorationType.Before), ]); let inlineDecorations2 = viewModel.getViewLineRenderingData( @@ -216,52 +132,16 @@ suite('ViewModelDecorations', () => { ).inlineDecorations; // view line 3 (24 -> 36) - assert.deepEqual(inlineDecorations2, [ - { - range: new Range(1, 2, 3, 13), - inlineClassName: 'i-dec4', - type: InlineDecorationType.Regular - }, - { - range: new Range(3, 13, 3, 13), - inlineClassName: 'a-dec4', - type: InlineDecorationType.After - }, - { - range: new Range(1, 2, 5, 8), - inlineClassName: 'i-dec5', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 1, 3, 13), - inlineClassName: 'i-dec8', - type: InlineDecorationType.Regular - }, - { - range: new Range(3, 13, 3, 13), - inlineClassName: 'a-dec8', - type: InlineDecorationType.After - }, - { - range: new Range(2, 1, 5, 8), - inlineClassName: 'i-dec9', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 3, 3, 13), - inlineClassName: 'i-dec11', - type: InlineDecorationType.Regular - }, - { - range: new Range(3, 13, 3, 13), - inlineClassName: 'a-dec11', - type: InlineDecorationType.After - }, - { - range: new Range(2, 3, 5, 8), - inlineClassName: 'i-dec12', - type: InlineDecorationType.Regular - }, + assert.deepStrictEqual(inlineDecorations2, [ + new InlineDecoration(new Range(1, 2, 3, 13), 'i-dec4', InlineDecorationType.Regular), + new InlineDecoration(new Range(3, 13, 3, 13), 'a-dec4', InlineDecorationType.After), + new InlineDecoration(new Range(1, 2, 5, 8), 'i-dec5', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 1, 3, 13), 'i-dec8', InlineDecorationType.Regular), + new InlineDecoration(new Range(3, 13, 3, 13), 'a-dec8', InlineDecorationType.After), + new InlineDecoration(new Range(2, 1, 5, 8), 'i-dec9', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 3, 3, 13), 'i-dec11', InlineDecorationType.Regular), + new InlineDecoration(new Range(3, 13, 3, 13), 'a-dec11', InlineDecorationType.After), + new InlineDecoration(new Range(2, 3, 5, 8), 'i-dec12', InlineDecorationType.Regular), ]); }); }); @@ -275,11 +155,11 @@ suite('ViewModelDecorations', () => { wordWrapColumn: 13 }; testViewModel(text, opts, (viewModel, model) => { - assert.equal(viewModel.getLineContent(1), 'hello world, '); - assert.equal(viewModel.getLineContent(2), 'this is a '); - assert.equal(viewModel.getLineContent(3), 'buffer that '); - assert.equal(viewModel.getLineContent(4), 'will be '); - assert.equal(viewModel.getLineContent(5), 'wrapped'); + assert.strictEqual(viewModel.getLineContent(1), 'hello world, '); + assert.strictEqual(viewModel.getLineContent(2), 'this is a '); + assert.strictEqual(viewModel.getLineContent(3), 'buffer that '); + assert.strictEqual(viewModel.getLineContent(4), 'will be '); + assert.strictEqual(viewModel.getLineContent(5), 'wrapped'); model.changeDecorations((accessor) => { accessor.addDecoration( @@ -293,19 +173,19 @@ suite('ViewModelDecorations', () => { let decorations = viewModel.getDecorationsInViewport( new Range(2, viewModel.getLineMinColumn(2), 3, viewModel.getLineMaxColumn(3)) ).filter(x => Boolean(x.options.beforeContentClassName)); - assert.deepEqual(decorations, []); + assert.deepStrictEqual(decorations, []); let inlineDecorations1 = viewModel.getViewLineRenderingData( new Range(2, viewModel.getLineMinColumn(2), 3, viewModel.getLineMaxColumn(3)), 2 ).inlineDecorations; - assert.deepEqual(inlineDecorations1, []); + assert.deepStrictEqual(inlineDecorations1, []); let inlineDecorations2 = viewModel.getViewLineRenderingData( new Range(2, viewModel.getLineMinColumn(2), 3, viewModel.getLineMaxColumn(3)), 3 ).inlineDecorations; - assert.deepEqual(inlineDecorations2, []); + assert.deepStrictEqual(inlineDecorations2, []); }); }); @@ -329,17 +209,9 @@ suite('ViewModelDecorations', () => { new Range(1, 1, 1, 1), 1 ).inlineDecorations; - assert.deepEqual(inlineDecorations, [ - { - range: new Range(1, 1, 1, 1), - inlineClassName: 'before1', - type: InlineDecorationType.Before - }, - { - range: new Range(1, 1, 1, 1), - inlineClassName: 'after1', - type: InlineDecorationType.After - } + assert.deepStrictEqual(inlineDecorations, [ + new InlineDecoration(new Range(1, 1, 1, 1), 'before1', InlineDecorationType.Before), + new InlineDecoration(new Range(1, 1, 1, 1), 'after1', InlineDecorationType.After) ]); }); }); diff --git a/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts b/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts index 6fe30a12..d556f3da 100644 --- a/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts +++ b/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts @@ -18,7 +18,7 @@ suite('ViewModel', () => { lineNumbersMinChars: 1 }; testViewModel(text, opts, (viewModel, model) => { - assert.equal(viewModel.getLineCount(), 1); + assert.strictEqual(viewModel.getLineCount(), 1); viewModel.setViewport(1, 1, 1); @@ -38,14 +38,14 @@ suite('ViewModel', () => { ].join('\n') }]); - assert.equal(viewModel.getLineCount(), 10); + assert.strictEqual(viewModel.getLineCount(), 10); }); }); test('issue #44805: SplitLinesCollection: attempt to access a \'newer\' model', () => { const text = ['']; testViewModel(text, {}, (viewModel, model) => { - assert.equal(viewModel.getLineCount(), 1); + assert.strictEqual(viewModel.getLineCount(), 1); model.pushEditOperations([], [{ range: new Range(1, 1, 1, 1), @@ -74,7 +74,7 @@ suite('ViewModel', () => { model.undo(); viewLineCount.push(viewModel.getLineCount()); - assert.deepEqual(viewLineCount, [4, 1, 1, 1, 1]); + assert.deepStrictEqual(viewLineCount, [4, 1, 1, 1, 1]); }); }); @@ -85,7 +85,7 @@ suite('ViewModel', () => { 'line3' ]; testViewModel(text, {}, (viewModel, model) => { - assert.equal(viewModel.getLineCount(), 3); + assert.strictEqual(viewModel.getLineCount(), 3); viewModel.setHiddenAreas([new Range(1, 1, 3, 1)]); assert.ok(viewModel.getVisibleRanges() !== null); }); @@ -96,7 +96,7 @@ suite('ViewModel', () => { '' ]; testViewModel(text, {}, (viewModel, model) => { - assert.equal(viewModel.getLineCount(), 1); + assert.strictEqual(viewModel.getLineCount(), 1); model.pushEditOperations([], [{ range: new Range(1, 1, 1, 1), @@ -104,7 +104,7 @@ suite('ViewModel', () => { }], () => ([])); viewModel.setHiddenAreas([new Range(1, 1, 1, 1)]); - assert.equal(viewModel.getLineCount(), 2); + assert.strictEqual(viewModel.getLineCount(), 2); model.undo(); assert.ok(viewModel.getVisibleRanges() !== null); @@ -114,7 +114,7 @@ suite('ViewModel', () => { function assertGetPlainTextToCopy(text: string[], ranges: Range[], emptySelectionClipboard: boolean, expected: string | string[]): void { testViewModel(text, {}, (viewModel, model) => { let actual = viewModel.getPlainTextToCopy(ranges, emptySelectionClipboard, false); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); } @@ -259,7 +259,39 @@ suite('ViewModel', () => { testViewModel(USUAL_TEXT, {}, (viewModel, model) => { model.setEOL(EndOfLineSequence.LF); let actual = viewModel.getPlainTextToCopy([new Range(2, 1, 5, 1)], true, true); - assert.deepEqual(actual, 'line2\r\nline3\r\nline4\r\n'); + assert.deepStrictEqual(actual, 'line2\r\nline3\r\nline4\r\n'); }); }); + + test('issue #40926: Incorrect spacing when inserting new line after multiple folded blocks of code', () => { + testViewModel( + [ + 'foo = {', + ' foobar: function() {', + ' this.foobar();', + ' },', + ' foobar: function() {', + ' this.foobar();', + ' },', + ' foobar: function() {', + ' this.foobar();', + ' },', + '}', + ], {}, (viewModel, model) => { + viewModel.setHiddenAreas([ + new Range(3, 1, 3, 1), + new Range(6, 1, 6, 1), + new Range(9, 1, 9, 1), + ]); + + model.applyEdits([ + { range: new Range(4, 7, 4, 7), text: '\n ' }, + { range: new Range(7, 7, 7, 7), text: '\n ' }, + { range: new Range(10, 7, 10, 7), text: '\n ' } + ]); + + assert.strictEqual(viewModel.getLineCount(), 11); + } + ); + }); }); diff --git a/src/vs/editor/test/node/classification/typescript.test.ts b/src/vs/editor/test/node/classification/typescript.test.ts index 5e399061..19c7b901 100644 --- a/src/vs/editor/test/node/classification/typescript.test.ts +++ b/src/vs/editor/test/node/classification/typescript.test.ts @@ -126,7 +126,7 @@ function executeTest(fileName: string, parseFunc: IParseFunc): void { actual[3 * actualIndex] + actual[3 * actualIndex + 1] >= assertion.startOffset + assertion.length, `Line ${assertion.testLineNumber} : length : ${actual[3 * actualIndex]} + ${actual[3 * actualIndex + 1]} >= ${assertion.startOffset} + ${assertion.length}.` ); - assert.equal( + assert.strictEqual( actual[3 * actualIndex + 2], assertion.tokenType, `Line ${assertion.testLineNumber} : tokenType`); diff --git a/src/vs/loader.js b/src/vs/loader.js index 76db9773..a9ac0018 100644 --- a/src/vs/loader.js +++ b/src/vs/loader.js @@ -36,7 +36,7 @@ var AMDLoader; this._detect(); return this._isWindows; }, - enumerable: true, + enumerable: false, configurable: true }); Object.defineProperty(Environment.prototype, "isNode", { @@ -44,7 +44,7 @@ var AMDLoader; this._detect(); return this._isNode; }, - enumerable: true, + enumerable: false, configurable: true }); Object.defineProperty(Environment.prototype, "isElectronRenderer", { @@ -52,7 +52,7 @@ var AMDLoader; this._detect(); return this._isElectronRenderer; }, - enumerable: true, + enumerable: false, configurable: true }); Object.defineProperty(Environment.prototype, "isWebWorker", { @@ -60,7 +60,7 @@ var AMDLoader; this._detect(); return this._isWebWorker; }, - enumerable: true, + enumerable: false, configurable: true }); Environment.prototype._detect = function () { @@ -199,6 +199,7 @@ var AMDLoader; return obj; } if (!Array.isArray(obj) && Object.getPrototypeOf(obj) !== Object.prototype) { + // only clone "simple" objects return obj; } var result = Array.isArray(obj) ? [] : {}; @@ -740,8 +741,8 @@ var AMDLoader; // nothing } }; - require.resolve = function resolve(request) { - return Module._resolveFilename(request, mod); + require.resolve = function resolve(request, options) { + return Module._resolveFilename(request, mod, false, options); }; require.main = process.mainModule; require.extensions = Module._extensions; @@ -862,7 +863,7 @@ var AMDLoader; } }; NodeScriptLoader.prototype._getCachedDataPath = function (config, filename) { - var hash = this._crypto.createHash('md5').update(filename, 'utf8').update(config.seed, 'utf8').digest('hex'); + var hash = this._crypto.createHash('md5').update(filename, 'utf8').update(config.seed, 'utf8').update(process.arch, '').digest('hex'); var basename = this._path.basename(filename).replace(/\.js$/, ''); return this._path.join(config.path, basename + "-" + hash + ".code"); }; @@ -1217,6 +1218,7 @@ var AMDLoader; this._requireFunc = requireFunc; this._moduleIdProvider = new ModuleIdProvider(); this._config = new AMDLoader.Configuration(this._env); + this._hasDependencyCycle = false; this._modules2 = []; this._knownModules2 = []; this._inverseDependencies2 = []; @@ -1561,6 +1563,9 @@ var AMDLoader; result.getStats = function () { return _this.getLoaderEvents(); }; + result.hasDependencyCycle = function () { + return _this._hasDependencyCycle; + }; result.config = function (params, shouldOverwrite) { if (shouldOverwrite === void 0) { shouldOverwrite = false; } _this.configure(params, shouldOverwrite); @@ -1666,6 +1671,7 @@ var AMDLoader; continue; } if (this._hasDependencyPath(dependency.id, module.id)) { + this._hasDependencyCycle = true; console.warn('There is a dependency cycle between \'' + this._moduleIdProvider.getStrModuleId(dependency.id) + '\' and \'' + this._moduleIdProvider.getStrModuleId(module.id) + '\'. The cyclic path follows:'); var cyclePath = this._findCyclePath(dependency.id, module.id, 0) || []; cyclePath.reverse(); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 2cb6913f..227452ee 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -10,6 +10,7 @@ declare namespace monaco { export type Thenable = PromiseLike; export interface Environment { + globalAPI?: boolean; baseUrl?: string; getWorker?(workerId: string, label: string): Worker; getWorkerUrl?(workerId: string, label: string): string; @@ -897,6 +898,12 @@ declare namespace monaco.editor { take?: number; }): IMarker[]; + /** + * Emitted when markers change for a model. + * @event + */ + export function onDidChangeMarkers(listener: (e: readonly Uri[]) => void): IDisposable; + /** * Get the model that has `uri` if it exists. */ @@ -969,6 +976,11 @@ declare namespace monaco.editor { */ export function remeasureFonts(): void; + /** + * Register a command. + */ + export function registerCommand(id: string, handler: (accessor: any, ...args: any[]) => void): IDisposable; + export type BuiltinTheme = 'vs' | 'vs-dark' | 'hc-black'; export interface IStandaloneThemeData { @@ -3176,6 +3188,10 @@ declare namespace monaco.editor { * Controls strikethrough deprecated variables. */ showDeprecated?: boolean; + /** + * Control the behavior and rendering of the inline hints. + */ + inlineHints?: IEditorInlineHintsOptions; } /** @@ -3222,6 +3238,11 @@ declare namespace monaco.editor { * Defaults to false */ isInEmbeddedEditor?: boolean; + /** + * Is the diff editor should render overview ruler + * Defaults to true + */ + renderOverviewRuler?: boolean; /** * Control the wrapping of the diff editor. */ @@ -3522,6 +3543,29 @@ declare namespace monaco.editor { export type EditorLightbulbOptions = Readonly>; + /** + * Configuration options for editor inlineHints + */ + export interface IEditorInlineHintsOptions { + /** + * Enable the inline hints. + * Defaults to true. + */ + enabled?: boolean; + /** + * Font size of inline hints. + * Default to 90% of the editor font size. + */ + fontSize?: number; + /** + * Font family of inline hints. + * Defaults to editor font family. + */ + fontFamily?: string; + } + + export type EditorInlineHintsOptions = Readonly>; + /** * Configuration options for editor minimap */ @@ -4023,11 +4067,12 @@ declare namespace monaco.editor { wrappingIndent = 117, wrappingStrategy = 118, showDeprecated = 119, - editorClassName = 120, - pixelRatio = 121, - tabFocusMode = 122, - layoutInfo = 123, - wrappingInfo = 124 + inlineHints = 120, + editorClassName = 121, + pixelRatio = 122, + tabFocusMode = 123, + layoutInfo = 124, + wrappingInfo = 125 } export const EditorOptions: { acceptSuggestionOnCommitCharacter: IEditorOption; @@ -4128,6 +4173,7 @@ declare namespace monaco.editor { showFoldingControls: IEditorOption; showUnused: IEditorOption; showDeprecated: IEditorOption; + inlineHints: IEditorOption; snippetSuggestions: IEditorOption; smartSelect: IEditorOption; smoothScrolling: IEditorOption; @@ -4492,6 +4538,10 @@ declare namespace monaco.editor { } export interface IDiffEditorConstructionOptions extends IDiffEditorOptions { + /** + * The initial editor dimension (to avoid measuring the container). + */ + dimension?: IDimension; /** * Place overflow widgets inside an external DOM node. * Defaults to an internal DOM node. @@ -4935,6 +4985,7 @@ declare namespace monaco.editor { export class FontInfo extends BareFontInfo { readonly _editorStylingBrand: void; + readonly version: number; readonly isTrusted: boolean; readonly isMonospace: boolean; readonly typicalHalfwidthCharacterWidth: number; @@ -4949,6 +5000,7 @@ declare namespace monaco.editor { export class BareFontInfo { readonly _bareFontInfoBrand: void; readonly zoomLevel: number; + readonly pixelRatio: number; readonly fontFamily: string; readonly fontWeight: string; readonly fontSize: number; @@ -5075,6 +5127,12 @@ declare namespace monaco.languages { tokenize?(line: string, state: IState): ILineTokens; } + /** + * Change the color map that is used for token colors. + * Supported formats (hex): #RRGGBB, $RRGGBBAA, #RGB, #RGBA + */ + export function setColorMap(colorMap: string[] | null): void; + /** * Set the tokens provider for a language (manual implementation). */ @@ -5366,7 +5424,7 @@ declare namespace monaco.languages { /** * This rule will only execute if the text above the this line matches this regular expression. */ - oneLineAboveText?: RegExp; + previousLineText?: RegExp; /** * The action to execute. */ @@ -6352,6 +6410,19 @@ declare namespace monaco.languages { resolveCodeLens?(model: editor.ITextModel, codeLens: CodeLens, token: CancellationToken): ProviderResult; } + export interface InlineHint { + text: string; + range: IRange; + description?: string | IMarkdownString; + whitespaceBefore?: boolean; + whitespaceAfter?: boolean; + } + + export interface InlineHintsProvider { + onDidChangeInlineHints?: IEvent | undefined; + provideInlineHints(model: editor.ITextModel, range: Range, token: CancellationToken): ProviderResult; + } + export interface SemanticTokensLegend { readonly tokenTypes: string[]; readonly tokenModifiers: string[]; @@ -6429,6 +6500,11 @@ declare namespace monaco.languages { * attach this to every token class (by default '.' + name) */ tokenPostfix?: string; + /** + * include line feeds (in the form of a \n character) at the end of lines + * Defaults to false + */ + includeLF?: boolean; } /** diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.css b/src/vs/platform/actions/browser/menuEntryActionViewItem.css new file mode 100644 index 00000000..5ef85311 --- /dev/null +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.css @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-action-bar .action-item.menu-entry .action-label { + background-image: var(--menu-entry-icon-light); + display: inline-flex; +} + +.vs-dark .monaco-action-bar .action-item.menu-entry .action-label, +.hc-black .monaco-action-bar .action-item.menu-entry .action-label { + background-image: var(--menu-entry-icon-dark); +} diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 1344ea90..fbfdd035 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createCSSRule, asCSSUrl, ModifierKeyEmitter } from 'vs/base/browser/dom'; +import 'vs/css!./menuEntryActionViewItem'; +import { asCSSUrl, ModifierKeyEmitter } from 'vs/base/browser/dom'; import { domEvent } from 'vs/base/browser/event'; import { IAction, Separator } from 'vs/base/common/actions'; -import { IdGenerator } from 'vs/base/common/idGenerator'; import { IDisposable, toDisposable, MutableDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { ICommandAction, IMenu, IMenuActionOptions, MenuItemAction, SubmenuItemAction, Icon } from 'vs/platform/actions/common/actions'; @@ -17,6 +17,7 @@ import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; import { isWindows, isLinux } from 'vs/base/common/platform'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, isPrimaryGroup?: (group: string) => boolean): IDisposable { const groups = menu.getActions(options); @@ -44,8 +45,7 @@ function asDisposable(groups: ReadonlyArray<[string, ReadonlyArray]>, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, useAlternativeActions: boolean, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void { - for (let tuple of groups) { - let [group, actions] = tuple; + for (let [group, actions] of groups) { if (useAlternativeActions) { actions = actions.map(a => (a instanceof MenuItemAction) && !!a.alt ? a.alt : a); } @@ -66,10 +66,6 @@ function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray(); - export class MenuEntryActionViewItem extends ActionViewItem { private _wantsAltCommand: boolean = false; @@ -85,7 +81,7 @@ export class MenuEntryActionViewItem extends ActionViewItem { this._altKey = ModifierKeyEmitter.getInstance(); } - protected get _commandAction(): IAction { + protected get _commandAction(): MenuItemAction { return this._wantsAltCommand && (this._action).alt || this._action; } @@ -93,12 +89,14 @@ export class MenuEntryActionViewItem extends ActionViewItem { event.preventDefault(); event.stopPropagation(); - this.actionRunner.run(this._commandAction, this._context) - .then(undefined, err => this._notificationService.error(err)); + this.actionRunner + .run(this._commandAction, this._context) + .catch(err => this._notificationService.error(err)); } render(container: HTMLElement): void { super.render(container); + container.classList.add('menu-entry'); this._updateItemClass(this._action.item); @@ -167,46 +165,39 @@ export class MenuEntryActionViewItem extends ActionViewItem { private _updateItemClass(item: ICommandAction): void { this._itemClassDispose.value = undefined; + const { element, label } = this; + if (!element || !label) { + return; + } + const icon = this._commandAction.checked && (item.toggled as { icon?: Icon })?.icon ? (item.toggled as { icon: Icon }).icon : item.icon; + if (!icon) { + return; + } + if (ThemeIcon.isThemeIcon(icon)) { // theme icons const iconClass = ThemeIcon.asClassName(icon); - if (this.label && iconClass) { - this.label.classList.add(...iconClass.split(' ')); - this._itemClassDispose.value = toDisposable(() => { - if (this.label) { - this.label.classList.remove(...iconClass.split(' ')); - } - }); + label.classList.add(...iconClass.split(' ')); + this._itemClassDispose.value = toDisposable(() => { + label.classList.remove(...iconClass.split(' ')); + }); + + } else { + // icon path/url + if (icon.light) { + label.style.setProperty('--menu-entry-icon-light', asCSSUrl(icon.light)); } - - } else if (icon) { - // icon path - let iconClass: string; - - if (icon.dark?.scheme) { - - const iconPathMapKey = icon.dark.toString(); - - if (ICON_PATH_TO_CSS_RULES.has(iconPathMapKey)) { - iconClass = ICON_PATH_TO_CSS_RULES.get(iconPathMapKey)!; - } else { - iconClass = ids.nextId(); - createCSSRule(`.icon.${iconClass}`, `background-image: ${asCSSUrl(icon.light || icon.dark)}`); - createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: ${asCSSUrl(icon.dark)}`); - ICON_PATH_TO_CSS_RULES.set(iconPathMapKey, iconClass); - } - - if (this.label) { - this.label.classList.add('icon', ...iconClass.split(' ')); - this._itemClassDispose.value = toDisposable(() => { - if (this.label) { - this.label.classList.remove('icon', ...iconClass.split(' ')); - } - }); - } + if (icon.dark) { + label.style.setProperty('--menu-entry-icon-dark', asCSSUrl(icon.dark)); } + label.classList.add('icon'); + this._itemClassDispose.value = toDisposable(() => { + label.classList.remove('icon'); + label.style.removeProperty('--menu-entry-icon-light'); + label.style.removeProperty('--menu-entry-icon-dark'); + }); } } } @@ -215,29 +206,41 @@ export class SubmenuEntryActionViewItem extends DropdownMenuActionViewItem { constructor( action: SubmenuItemAction, - @INotificationService _notificationService: INotificationService, - @IContextMenuService _contextMenuService: IContextMenuService + @IContextMenuService contextMenuService: IContextMenuService ) { - let classNames: string | string[] | undefined; + super(action, { getActions: () => action.actions }, contextMenuService, { + menuAsChild: true, + classNames: ThemeIcon.isThemeIcon(action.item.icon) ? ThemeIcon.asClassName(action.item.icon) : undefined, + }); + } - if (action.item.icon) { - if (ThemeIcon.isThemeIcon(action.item.icon)) { - classNames = ThemeIcon.asClassName(action.item.icon)!; - } else if (action.item.icon.dark?.scheme) { - const iconPathMapKey = action.item.icon.dark.toString(); - - if (ICON_PATH_TO_CSS_RULES.has(iconPathMapKey)) { - classNames = ['icon', ICON_PATH_TO_CSS_RULES.get(iconPathMapKey)!]; - } else { - const className = ids.nextId(); - classNames = ['icon', className]; - createCSSRule(`.icon.${className}`, `background-image: ${asCSSUrl(action.item.icon.light || action.item.icon.dark)}`); - createCSSRule(`.vs-dark .icon.${className}, .hc-black .icon.${className}`, `background-image: ${asCSSUrl(action.item.icon.dark)}`); - ICON_PATH_TO_CSS_RULES.set(iconPathMapKey, className); + render(container: HTMLElement): void { + super.render(container); + if (this.element) { + container.classList.add('menu-entry'); + const { icon } = (this._action).item; + if (icon && !ThemeIcon.isThemeIcon(icon)) { + this.element.classList.add('icon'); + if (icon.light) { + this.element.style.setProperty('--menu-entry-icon-light', asCSSUrl(icon.light)); + } + if (icon.dark) { + this.element.style.setProperty('--menu-entry-icon-dark', asCSSUrl(icon.dark)); } } } - - super(action, action.actions, _contextMenuService, { classNames: classNames, menuAsChild: true }); + } +} + +/** + * Creates action view items for menu actions or submenu actions. + */ +export function createActionViewItem(instaService: IInstantiationService, action: IAction): undefined | MenuEntryActionViewItem | SubmenuEntryActionViewItem { + if (action instanceof MenuItemAction) { + return instaService.createInstance(MenuEntryActionViewItem, action); + } else if (action instanceof SubmenuItemAction) { + return instaService.createInstance(SubmenuEntryActionViewItem, action); + } else { + return undefined; } } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index c06a3511..514fc0bd 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -16,22 +16,36 @@ import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { UriDto } from 'vs/base/common/types'; import { Iterable } from 'vs/base/common/iterator'; import { LinkedList } from 'vs/base/common/linkedList'; +import { CSSIcon } from 'vs/base/common/codicons'; export interface ILocalizedString { + /** + * The localized value of the string. + */ value: string; + /** + * The original (non localized value of the string) + */ original: string; } +export interface ICommandActionTitle extends ILocalizedString { + /** + * The title with a mnemonic designation. && precedes the mnemonic. + */ + mnemonicTitle?: string; +} + export type Icon = { dark?: URI; light?: URI; } | ThemeIcon; export interface ICommandAction { id: string; - title: string | ILocalizedString; + title: string | ICommandActionTitle; category?: string | ILocalizedString; - tooltip?: string | ILocalizedString; + tooltip?: string; icon?: Icon; precondition?: ContextKeyExpression; - toggled?: ContextKeyExpression | { condition: ContextKeyExpression, icon?: Icon, tooltip?: string | ILocalizedString }; + toggled?: ContextKeyExpression | { condition: ContextKeyExpression, icon?: Icon, tooltip?: string }; } export type ISerializableCommandAction = UriDto; @@ -45,7 +59,7 @@ export interface IMenuItem { } export interface ISubmenuItem { - title: string | ILocalizedString; + title: string | ICommandActionTitle; submenu: MenuId; icon?: Icon; when?: ContextKeyExpression; @@ -112,6 +126,7 @@ export class MenuId { static readonly TunnelInline = new MenuId('TunnelInline'); static readonly TunnelTitle = new MenuId('TunnelTitle'); static readonly ViewItemContext = new MenuId('ViewItemContext'); + static readonly ViewContainerTitle = new MenuId('ViewContainerTitle'); static readonly ViewContainerTitleContext = new MenuId('ViewContainerTitleContext'); static readonly ViewTitle = new MenuId('ViewTitle'); static readonly ViewTitleContext = new MenuId('ViewTitleContext'); @@ -132,6 +147,7 @@ export class MenuId { static readonly TimelineTitle = new MenuId('TimelineTitle'); static readonly TimelineTitleContext = new MenuId('TimelineTitleContext'); static readonly AccountsContext = new MenuId('AccountsContext'); + static readonly PanelTitle = new MenuId('PanelTitle'); readonly id: number; readonly _debugName: string; @@ -148,7 +164,7 @@ export interface IMenuActionOptions { } export interface IMenu extends IDisposable { - readonly onDidChange: Event; + readonly onDidChange: Event; getActions(options?: IMenuActionOptions): [string, Array][]; } @@ -334,61 +350,71 @@ export class SubmenuItemAction extends SubmenuAction { } } -export class MenuItemAction extends ExecuteCommandAction { +// implements IAction, does NOT extend Action, so that no one +// subscribes to events of Action or modified properties +export class MenuItemAction implements IAction { readonly item: ICommandAction; readonly alt: MenuItemAction | undefined; - private _options: IMenuActionOptions; + private readonly _options: IMenuActionOptions | undefined; + + readonly id: string; + readonly label: string; + readonly tooltip: string; + readonly class: string | undefined; + readonly enabled: boolean; + readonly checked: boolean; constructor( item: ICommandAction, alt: ICommandAction | undefined, - options: IMenuActionOptions, + options: IMenuActionOptions | undefined, @IContextKeyService contextKeyService: IContextKeyService, - @ICommandService commandService: ICommandService + @ICommandService private _commandService: ICommandService ) { - typeof item.title === 'string' ? super(item.id, item.title, commandService) : super(item.id, item.title.value, commandService); - - this._cssClass = undefined; - this._enabled = !item.precondition || contextKeyService.contextMatchesRules(item.precondition); - this._tooltip = item.tooltip ? typeof item.tooltip === 'string' ? item.tooltip : item.tooltip.value : undefined; + this.id = item.id; + this.label = typeof item.title === 'string' ? item.title : item.title.value; + this.tooltip = item.tooltip ?? ''; + this.enabled = !item.precondition || contextKeyService.contextMatchesRules(item.precondition); + this.checked = false; if (item.toggled) { const toggled = ((item.toggled as { condition: ContextKeyExpression }).condition ? item.toggled : { condition: item.toggled }) as { condition: ContextKeyExpression, icon?: Icon, tooltip?: string | ILocalizedString }; - this._checked = contextKeyService.contextMatchesRules(toggled.condition); - if (this._checked && toggled.tooltip) { - this._tooltip = typeof toggled.tooltip === 'string' ? toggled.tooltip : toggled.tooltip.value; + this.checked = contextKeyService.contextMatchesRules(toggled.condition); + if (this.checked && toggled.tooltip) { + this.tooltip = typeof toggled.tooltip === 'string' ? toggled.tooltip : toggled.tooltip.value; } } - this._options = options || {}; - this.item = item; - this.alt = alt ? new MenuItemAction(alt, undefined, this._options, contextKeyService, commandService) : undefined; + this.alt = alt ? new MenuItemAction(alt, undefined, options, contextKeyService, _commandService) : undefined; + this._options = options; + if (ThemeIcon.isThemeIcon(item.icon)) { + this.class = CSSIcon.asClassName(item.icon); + } } dispose(): void { - if (this.alt) { - this.alt.dispose(); - } - super.dispose(); + // there is NOTHING to dispose and the MenuItemAction should + // never have anything to dispose as it is a convenience type + // to bridge into the rendering world. } run(...args: any[]): Promise { let runArgs: any[] = []; - if (this._options.arg) { + if (this._options?.arg) { runArgs = [...runArgs, this._options.arg]; } - if (this._options.shouldForwardArgs) { + if (this._options?.shouldForwardArgs) { runArgs = [...runArgs, ...args]; } - return super.run(...runArgs); + return this._commandService.executeCommand(this.id, ...runArgs); } } diff --git a/src/vs/platform/actions/common/menuService.ts b/src/vs/platform/actions/common/menuService.ts index 6ea4c5a0..eaabf117 100644 --- a/src/vs/platform/actions/common/menuService.ts +++ b/src/vs/platform/actions/common/menuService.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { RunOnceScheduler } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IMenu, IMenuActionOptions, IMenuItem, IMenuService, isIMenuItem, ISubmenuItem, MenuId, MenuItemAction, MenuRegistry, SubmenuItemAction, ILocalizedString } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IContextKeyService, IContextKeyChangeEvent, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; export class MenuService implements IMenuService { @@ -29,9 +30,11 @@ type MenuItemGroup = [string, Array]; class Menu implements IMenu { - private readonly _onDidChange = new Emitter(); private readonly _dispoables = new DisposableStore(); + private readonly _onDidChange = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + private _menuGroups: MenuItemGroup[] = []; private _contextKeys: Set = new Set(); @@ -45,19 +48,23 @@ class Menu implements IMenu { // rebuild this menu whenever the menu registry reports an // event for this MenuId - this._dispoables.add(Event.debounce( - Event.filter(MenuRegistry.onDidChangeMenu, set => set.has(this._id)), - () => { }, - 50 - )(this._build, this)); + const rebuildMenuSoon = new RunOnceScheduler(() => this._build(), 50); + this._dispoables.add(rebuildMenuSoon); + this._dispoables.add(MenuRegistry.onDidChangeMenu(e => { + if (e.has(_id)) { + rebuildMenuSoon.schedule(); + } + })); // when context keys change we need to check if the menu also // has changed - this._dispoables.add(Event.debounce( - this._contextKeyService.onDidChangeContext, - (last, event) => last || event.affectsSome(this._contextKeys), - 50 - )(e => e && this._onDidChange.fire(undefined), this)); + const fireChangeSoon = new RunOnceScheduler(() => this._onDidChange.fire(this), 50); + this._dispoables.add(fireChangeSoon); + this._dispoables.add(_contextKeyService.onDidChangeContext(e => { + if (e.affectsSome(this._contextKeys)) { + fireChangeSoon.schedule(); + } + })); } dispose(): void { @@ -88,25 +95,22 @@ class Menu implements IMenu { // keep keys for eventing Menu._fillInKbExprKeys(item.when, this._contextKeys); - // keep precondition keys for event if applicable - if (isIMenuItem(item) && item.command.precondition) { - Menu._fillInKbExprKeys(item.command.precondition, this._contextKeys); - } - - // keep toggled keys for event if applicable - if (isIMenuItem(item) && item.command.toggled) { - const toggledExpression: ContextKeyExpression = (item.command.toggled as { condition: ContextKeyExpression }).condition || item.command.toggled; - Menu._fillInKbExprKeys(toggledExpression, this._contextKeys); + if (isIMenuItem(item)) { + // keep precondition keys for event if applicable + if (item.command.precondition) { + Menu._fillInKbExprKeys(item.command.precondition, this._contextKeys); + } + // keep toggled keys for event if applicable + if (item.command.toggled) { + const toggledExpression: ContextKeyExpression = (item.command.toggled as { condition: ContextKeyExpression }).condition || item.command.toggled; + Menu._fillInKbExprKeys(toggledExpression, this._contextKeys); + } } } this._onDidChange.fire(this); } - get onDidChange(): Event { - return this._onDidChange.event; - } - - getActions(options: IMenuActionOptions): [string, Array][] { + getActions(options?: IMenuActionOptions): [string, Array][] { const result: [string, Array][] = []; for (let group of this._menuGroups) { const [id, items] = group; diff --git a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts index 42e2636d..75b2cac3 100644 --- a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -20,39 +20,14 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { ConsoleLogMainService } from 'vs/platform/log/common/log'; import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { createHash } from 'crypto'; -import { getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { Schemas } from 'vs/base/common/network'; import { isEqual } from 'vs/base/common/resources'; -suite('BackupMainService', () => { +flakySuite('BackupMainService', () => { function assertEqualUris(actual: URI[], expected: URI[]) { - assert.deepEqual(actual.map(a => a.toString()), expected.map(a => a.toString())); - } - - const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupservice'); - const backupHome = path.join(parentDir, 'Backups'); - const backupWorkspacesPath = path.join(backupHome, 'workspaces.json'); - - const environmentService = new EnvironmentMainService(parseArgs(process.argv, OPTIONS)); - - class TestBackupMainService extends BackupMainService { - - constructor(backupHome: string, backupWorkspacesPath: string, configService: TestConfigurationService) { - super(environmentService, configService, new ConsoleLogMainService()); - - this.backupHome = backupHome; - this.workspacesJsonPath = backupWorkspacesPath; - } - - toBackupPath(arg: URI | string): string { - const id = arg instanceof URI ? super.getFolderHash(arg) : arg; - return path.join(this.backupHome, id); - } - - getFolderHash(folderUri: URI): string { - return super.getFolderHash(folderUri); - } + assert.deepStrictEqual(actual.map(a => a.toString()), expected.map(a => a.toString())); } function toWorkspace(path: string): IWorkspaceIdentifier { @@ -79,20 +54,23 @@ suite('BackupMainService', () => { }; } - async function ensureFolderExists(uri: URI): Promise { + function ensureFolderExists(uri: URI): Promise { if (!fs.existsSync(uri.fsPath)) { fs.mkdirSync(uri.fsPath); } + const backupFolder = service.toBackupPath(uri); - await createBackupFolder(backupFolder); + return createBackupFolder(backupFolder); } async function ensureWorkspaceExists(workspace: IWorkspaceIdentifier): Promise { if (!fs.existsSync(workspace.configPath.fsPath)) { await pfs.writeFile(workspace.configPath.fsPath, 'Hello'); } + const backupFolder = service.toBackupPath(workspace.id); await createBackupFolder(backupFolder); + return workspace; } @@ -111,29 +89,52 @@ suite('BackupMainService', () => { const fooFile = URI.file(platform.isWindows ? 'C:\\foo' : '/foo'); const barFile = URI.file(platform.isWindows ? 'C:\\bar' : '/bar'); - const existingTestFolder1 = URI.file(path.join(parentDir, 'folder1')); - - let service: TestBackupMainService; + let service: BackupMainService & { toBackupPath(arg: URI | string): string, getFolderHash(folderUri: URI): string }; let configService: TestConfigurationService; - setup(async () => { + let environmentService: EnvironmentMainService; + let testDir: string; + let backupHome: string; + let backupWorkspacesPath: string; + let existingTestFolder1: URI; + + setup(async () => { + testDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupmainservice'); + backupHome = path.join(testDir, 'Backups'); + backupWorkspacesPath = path.join(backupHome, 'workspaces.json'); + existingTestFolder1 = URI.file(path.join(testDir, 'folder1')); + + environmentService = new EnvironmentMainService(parseArgs(process.argv, OPTIONS)); - // Delete any existing backups completely and then re-create it. - await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); await pfs.mkdirp(backupHome); configService = new TestConfigurationService(); - service = new TestBackupMainService(backupHome, backupWorkspacesPath, configService); + service = new class TestBackupMainService extends BackupMainService { + constructor() { + super(environmentService, configService, new ConsoleLogMainService()); + + this.backupHome = backupHome; + this.workspacesJsonPath = backupWorkspacesPath; + } + + toBackupPath(arg: URI | string): string { + const id = arg instanceof URI ? super.getFolderHash(arg) : arg; + return path.join(this.backupHome, id); + } + + getFolderHash(folderUri: URI): string { + return super.getFolderHash(folderUri); + } + }; return service.initialize(); }); teardown(() => { - return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); + return pfs.rimraf(testDir); }); test('service validates backup workspaces on startup and cleans up (folder workspaces)', async function () { - this.timeout(1000 * 10); // increase timeout for this test // 1) backup workspace path does not exist service.registerFolderBackupSync(fooFile); @@ -170,22 +171,21 @@ suite('BackupMainService', () => { fs.mkdirSync(service.toBackupPath(barFile)); fs.mkdirSync(fileBackups); service.registerFolderBackupSync(fooFile); - assert.equal(service.getFolderBackupPaths().length, 1); - assert.equal(service.getEmptyWindowBackupPaths().length, 0); + assert.strictEqual(service.getFolderBackupPaths().length, 1); + assert.strictEqual(service.getEmptyWindowBackupPaths().length, 0); fs.writeFileSync(path.join(fileBackups, 'backup.txt'), ''); await service.initialize(); - assert.equal(service.getFolderBackupPaths().length, 0); - assert.equal(service.getEmptyWindowBackupPaths().length, 1); + assert.strictEqual(service.getFolderBackupPaths().length, 0); + assert.strictEqual(service.getEmptyWindowBackupPaths().length, 1); }); test('service validates backup workspaces on startup and cleans up (root workspaces)', async function () { - this.timeout(1000 * 10); // increase timeout for this test // 1) backup workspace path does not exist service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath)); service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(barFile.fsPath)); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); // 2) backup workspace path exists with empty contents within fs.mkdirSync(service.toBackupPath(fooFile)); @@ -193,7 +193,7 @@ suite('BackupMainService', () => { service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath)); service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(barFile.fsPath)); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); assert.ok(!fs.existsSync(service.toBackupPath(fooFile))); assert.ok(!fs.existsSync(service.toBackupPath(barFile))); @@ -205,7 +205,7 @@ suite('BackupMainService', () => { service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath)); service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(barFile.fsPath)); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); assert.ok(!fs.existsSync(service.toBackupPath(fooFile))); assert.ok(!fs.existsSync(service.toBackupPath(barFile))); @@ -216,12 +216,12 @@ suite('BackupMainService', () => { fs.mkdirSync(service.toBackupPath(barFile)); fs.mkdirSync(fileBackups); service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath)); - assert.equal(service.getWorkspaceBackups().length, 1); - assert.equal(service.getEmptyWindowBackupPaths().length, 0); + assert.strictEqual(service.getWorkspaceBackups().length, 1); + assert.strictEqual(service.getEmptyWindowBackupPaths().length, 0); fs.writeFileSync(path.join(fileBackups, 'backup.txt'), ''); await service.initialize(); - assert.equal(service.getWorkspaceBackups().length, 0); - assert.equal(service.getEmptyWindowBackupPaths().length, 1); + assert.strictEqual(service.getWorkspaceBackups().length, 0); + assert.strictEqual(service.getEmptyWindowBackupPaths().length, 1); }); test('service supports to migrate backup data from another location', () => { @@ -237,7 +237,7 @@ suite('BackupMainService', () => { assert.ok(!fs.existsSync(backupPathToMigrate)); const emptyBackups = service.getEmptyWindowBackupPaths(); - assert.equal(0, emptyBackups.length); + assert.strictEqual(0, emptyBackups.length); }); test('service backup migration makes sure to preserve existing backups', () => { @@ -258,8 +258,8 @@ suite('BackupMainService', () => { assert.ok(!fs.existsSync(backupPathToMigrate)); const emptyBackups = service.getEmptyWindowBackupPaths(); - assert.equal(1, emptyBackups.length); - assert.equal(1, fs.readdirSync(path.join(backupHome, emptyBackups[0].backupFolder!)).length); + assert.strictEqual(1, emptyBackups.length); + assert.strictEqual(1, fs.readdirSync(path.join(backupHome, emptyBackups[0].backupFolder!)).length); }); suite('loadSync', () => { @@ -315,121 +315,120 @@ suite('BackupMainService', () => { }); test('getWorkspaceBackups() should return [] when workspaces.json doesn\'t exist', () => { - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); }); test('getWorkspaceBackups() should return [] when workspaces.json is not properly formed JSON', async () => { fs.writeFileSync(backupWorkspacesPath, ''); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{]'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, 'foo'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); }); test('getWorkspaceBackups() should return [] when folderWorkspaces in workspaces.json is absent', async () => { fs.writeFileSync(backupWorkspacesPath, '{}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); }); test('getWorkspaceBackups() should return [] when rootWorkspaces in workspaces.json is not a object array', async () => { fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{"foo": ["bar"]}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{"foo": []}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{"foo": "bar"}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":"foo"}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":1}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); }); test('getWorkspaceBackups() should return [] when rootURIWorkspaces in workspaces.json is not a object array', async () => { fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{"foo": ["bar"]}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{"foo": []}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{"foo": "bar"}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":"foo"}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":1}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); }); test('getWorkspaceBackups() should return [] when files.hotExit = "onExitAndWindowClose"', async () => { const upperFooPath = fooFile.fsPath.toUpperCase(); service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(upperFooPath)); - assert.equal(service.getWorkspaceBackups().length, 1); + assert.strictEqual(service.getWorkspaceBackups().length, 1); assertEqualUris(service.getWorkspaceBackups().map(r => r.workspace.configPath), [URI.file(upperFooPath)]); configService.setUserConfiguration('files.hotExit', HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); }); test('getEmptyWorkspaceBackupPaths() should return [] when workspaces.json doesn\'t exist', () => { - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); }); test('getEmptyWorkspaceBackupPaths() should return [] when workspaces.json is not properly formed JSON', async () => { fs.writeFileSync(backupWorkspacesPath, ''); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{]'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, 'foo'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); }); test('getEmptyWorkspaceBackupPaths() should return [] when folderWorkspaces in workspaces.json is absent', async () => { fs.writeFileSync(backupWorkspacesPath, '{}'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); }); test('getEmptyWorkspaceBackupPaths() should return [] when folderWorkspaces in workspaces.json is not a string array', async function () { - this.timeout(5000); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{}}'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{"foo": ["bar"]}}'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{"foo": []}}'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{"foo": "bar"}}'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":"foo"}'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":1}'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); }); }); @@ -448,7 +447,7 @@ suite('BackupMainService', () => { const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); - assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); + assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); test('should ignore duplicates on Windows and Mac (folder workspace)', async () => { @@ -464,14 +463,13 @@ suite('BackupMainService', () => { await service.initialize(); const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); - assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); + assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); test('should ignore duplicates on Windows and Mac (root workspace)', async () => { - - const workspacePath = path.join(parentDir, 'Foo.code-workspace'); - const workspacePath1 = path.join(parentDir, 'FOO.code-workspace'); - const workspacePath2 = path.join(parentDir, 'foo.code-workspace'); + const workspacePath = path.join(testDir, 'Foo.code-workspace'); + const workspacePath1 = path.join(testDir, 'FOO.code-workspace'); + const workspacePath2 = path.join(testDir, 'foo.code-workspace'); const workspace1 = await ensureWorkspaceExists(toWorkspace(workspacePath)); const workspace2 = await ensureWorkspaceExists(toWorkspace(workspacePath1)); @@ -487,11 +485,11 @@ suite('BackupMainService', () => { const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); - assert.equal(json.rootURIWorkspaces.length, platform.isLinux ? 3 : 1); + assert.strictEqual(json.rootURIWorkspaces.length, platform.isLinux ? 3 : 1); if (platform.isLinux) { - assert.deepEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [URI.file(workspacePath).toString(), URI.file(workspacePath1).toString(), URI.file(workspacePath2).toString()]); + assert.deepStrictEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [URI.file(workspacePath).toString(), URI.file(workspacePath1).toString(), URI.file(workspacePath2).toString()]); } else { - assert.deepEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [URI.file(workspacePath).toString()], 'should return the first duplicated entry'); + assert.deepStrictEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [URI.file(workspacePath).toString()], 'should return the first duplicated entry'); } }); }); @@ -503,7 +501,7 @@ suite('BackupMainService', () => { assertEqualUris(service.getFolderBackupPaths(), [fooFile, barFile]); const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); - assert.deepEqual(json.folderURIWorkspaces, [fooFile.toString(), barFile.toString()]); + assert.deepStrictEqual(json.folderURIWorkspaces, [fooFile.toString(), barFile.toString()]); }); test('should persist paths to workspaces.json (root workspace)', async () => { @@ -513,15 +511,15 @@ suite('BackupMainService', () => { service.registerWorkspaceBackupSync(ws2); assertEqualUris(service.getWorkspaceBackups().map(b => b.workspace.configPath), [fooFile, barFile]); - assert.equal(ws1.workspace.id, service.getWorkspaceBackups()[0].workspace.id); - assert.equal(ws2.workspace.id, service.getWorkspaceBackups()[1].workspace.id); + assert.strictEqual(ws1.workspace.id, service.getWorkspaceBackups()[0].workspace.id); + assert.strictEqual(ws2.workspace.id, service.getWorkspaceBackups()[1].workspace.id); const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); - assert.deepEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [fooFile.toString(), barFile.toString()]); - assert.equal(ws1.workspace.id, json.rootURIWorkspaces[0].id); - assert.equal(ws2.workspace.id, json.rootURIWorkspaces[1].id); + assert.deepStrictEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [fooFile.toString(), barFile.toString()]); + assert.strictEqual(ws1.workspace.id, json.rootURIWorkspaces[0].id); + assert.strictEqual(ws2.workspace.id, json.rootURIWorkspaces[1].id); }); }); @@ -531,7 +529,7 @@ suite('BackupMainService', () => { const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); - assert.deepEqual(json.folderURIWorkspaces, [URI.file(fooFile.fsPath.toUpperCase()).toString()]); + assert.deepStrictEqual(json.folderURIWorkspaces, [URI.file(fooFile.fsPath.toUpperCase()).toString()]); }); test('should always store the workspace path in workspaces.json using the case given, regardless of whether the file system is case-sensitive (root workspace)', async () => { @@ -541,7 +539,7 @@ suite('BackupMainService', () => { const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); - assert.deepEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [URI.file(upperFooPath).toString()]); + assert.deepStrictEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [URI.file(upperFooPath).toString()]); }); suite('removeBackupPathSync', () => { @@ -552,12 +550,12 @@ suite('BackupMainService', () => { const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); - assert.deepEqual(json.folderURIWorkspaces, [barFile.toString()]); + assert.deepStrictEqual(json.folderURIWorkspaces, [barFile.toString()]); service.unregisterFolderBackupSync(barFile); const content = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json2 = (JSON.parse(content)); - assert.deepEqual(json2.folderURIWorkspaces, []); + assert.deepStrictEqual(json2.folderURIWorkspaces, []); }); test('should remove folder workspaces from workspaces.json (root workspace)', async () => { @@ -569,12 +567,12 @@ suite('BackupMainService', () => { const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); - assert.deepEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [barFile.toString()]); + assert.deepStrictEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [barFile.toString()]); service.unregisterWorkspaceBackupSync(ws2.workspace); const content = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json2 = (JSON.parse(content)); - assert.deepEqual(json2.rootURIWorkspaces, []); + assert.deepStrictEqual(json2.rootURIWorkspaces, []); }); test('should remove empty workspaces from workspaces.json', async () => { @@ -584,12 +582,12 @@ suite('BackupMainService', () => { const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); - assert.deepEqual(json.emptyWorkspaceInfos, [{ backupFolder: 'bar' }]); + assert.deepStrictEqual(json.emptyWorkspaceInfos, [{ backupFolder: 'bar' }]); service.unregisterEmptyWindowBackupSync('bar'); const content = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json2 = (JSON.parse(content)); - assert.deepEqual(json2.emptyWorkspaceInfos, []); + assert.deepStrictEqual(json2.emptyWorkspaceInfos, []); }); test('should fail gracefully when removing a path that doesn\'t exist', async () => { @@ -603,24 +601,18 @@ suite('BackupMainService', () => { service.unregisterEmptyWindowBackupSync('test'); const content = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(content)); - assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); + assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); }); suite('getWorkspaceHash', () => { - - test('should ignore case on Windows and Mac', () => { - // Skip test on Linux - if (platform.isLinux) { - return; - } - + (platform.isLinux ? test.skip : test)('should ignore case on Windows and Mac', () => { if (platform.isMacintosh) { - assert.equal(service.getFolderHash(URI.file('/foo')), service.getFolderHash(URI.file('/FOO'))); + assert.strictEqual(service.getFolderHash(URI.file('/foo')), service.getFolderHash(URI.file('/FOO'))); } if (platform.isWindows) { - assert.equal(service.getFolderHash(URI.file('c:\\foo')), service.getFolderHash(URI.file('C:\\FOO'))); + assert.strictEqual(service.getFolderHash(URI.file('c:\\foo')), service.getFolderHash(URI.file('C:\\FOO'))); } }); }); @@ -631,9 +623,9 @@ suite('BackupMainService', () => { service.registerFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase())); if (platform.isLinux) { - assert.equal(service.getFolderBackupPaths().length, 2); + assert.strictEqual(service.getFolderBackupPaths().length, 2); } else { - assert.equal(service.getFolderBackupPaths().length, 1); + assert.strictEqual(service.getFolderBackupPaths().length, 1); } }); @@ -642,9 +634,9 @@ suite('BackupMainService', () => { service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath.toUpperCase())); if (platform.isLinux) { - assert.equal(service.getWorkspaceBackups().length, 2); + assert.strictEqual(service.getWorkspaceBackups().length, 2); } else { - assert.equal(service.getWorkspaceBackups().length, 1); + assert.strictEqual(service.getWorkspaceBackups().length, 1); } }); @@ -653,16 +645,16 @@ suite('BackupMainService', () => { // same case service.registerFolderBackupSync(fooFile); service.unregisterFolderBackupSync(fooFile); - assert.equal(service.getFolderBackupPaths().length, 0); + assert.strictEqual(service.getFolderBackupPaths().length, 0); // mixed case service.registerFolderBackupSync(fooFile); service.unregisterFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase())); if (platform.isLinux) { - assert.equal(service.getFolderBackupPaths().length, 1); + assert.strictEqual(service.getFolderBackupPaths().length, 1); } else { - assert.equal(service.getFolderBackupPaths().length, 0); + assert.strictEqual(service.getFolderBackupPaths().length, 0); } }); }); @@ -674,7 +666,7 @@ suite('BackupMainService', () => { const backupWorkspaceInfo = toWorkspaceBackupInfo(fooFile.fsPath); const workspaceBackupPath = service.registerWorkspaceBackupSync(backupWorkspaceInfo); - assert.equal(((await service.getDirtyWorkspaces()).length), 0); + assert.strictEqual(((await service.getDirtyWorkspaces()).length), 0); try { await pfs.mkdirp(path.join(folderBackupPath, Schemas.file)); @@ -683,13 +675,13 @@ suite('BackupMainService', () => { // ignore - folder might exist already } - assert.equal(((await service.getDirtyWorkspaces()).length), 0); + assert.strictEqual(((await service.getDirtyWorkspaces()).length), 0); fs.writeFileSync(path.join(folderBackupPath, Schemas.file, '594a4a9d82a277a899d4713a5b08f504'), ''); fs.writeFileSync(path.join(workspaceBackupPath, Schemas.untitled, '594a4a9d82a277a899d4713a5b08f504'), ''); const dirtyWorkspaces = await service.getDirtyWorkspaces(); - assert.equal(dirtyWorkspaces.length, 2); + assert.strictEqual(dirtyWorkspaces.length, 2); let found = 0; for (const dirtyWorkpspace of dirtyWorkspaces) { @@ -704,7 +696,7 @@ suite('BackupMainService', () => { } } - assert.equal(found, 2); + assert.strictEqual(found, 2); }); }); }); diff --git a/src/vs/platform/configuration/common/configurationModels.ts b/src/vs/platform/configuration/common/configurationModels.ts index 5af10fc3..c6928a8a 100644 --- a/src/vs/platform/configuration/common/configurationModels.ts +++ b/src/vs/platform/configuration/common/configurationModels.ts @@ -16,7 +16,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { Disposable } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; import { IFileService } from 'vs/platform/files/common/files'; -import { dirname } from 'vs/base/common/resources'; +import { IExtUri } from 'vs/base/common/resources'; export class ConfigurationModel implements IConfigurationModel { @@ -348,11 +348,12 @@ export class UserSettings extends Disposable { constructor( private readonly userSettingsResource: URI, private readonly scopes: ConfigurationScope[] | undefined, + extUri: IExtUri, private readonly fileService: IFileService ) { super(); this.parser = new ConfigurationModelParser(this.userSettingsResource.toString(), this.scopes); - this._register(this.fileService.watch(dirname(this.userSettingsResource))); + this._register(this.fileService.watch(extUri.dirname(this.userSettingsResource))); this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.userSettingsResource))(() => this._onDidChange.fire())); } diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index 07680c0f..7504cb72 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -112,6 +112,13 @@ export interface IConfigurationPropertySchema extends IJSONSchema { scope?: ConfigurationScope; included?: boolean; tags?: string[]; + /** + * When enabled this setting is ignored during sync and user can override this. + */ + ignoreSync?: boolean; + /** + * When enabled this setting is ignored during sync and user cannot override this. + */ disallowSyncIgnore?: boolean; enumItemLabels?: string[]; } diff --git a/src/vs/platform/configuration/common/configurationService.ts b/src/vs/platform/configuration/common/configurationService.ts index 2e8b85b1..3fd1df3f 100644 --- a/src/vs/platform/configuration/common/configurationService.ts +++ b/src/vs/platform/configuration/common/configurationService.ts @@ -12,6 +12,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; import { RunOnceScheduler } from 'vs/base/common/async'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; export class ConfigurationService extends Disposable implements IConfigurationService, IDisposable { @@ -29,7 +30,7 @@ export class ConfigurationService extends Disposable implements IConfigurationSe fileService: IFileService ) { super(); - this.userConfiguration = this._register(new UserSettings(this.settingsResource, undefined, fileService)); + this.userConfiguration = this._register(new UserSettings(this.settingsResource, undefined, extUriBiasedIgnorePathCase, fileService)); this.configuration = new Configuration(new DefaultConfigurationModel(), new ConfigurationModel()); this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reloadConfiguration(), 50)); diff --git a/src/vs/platform/contextkey/browser/contextKeyService.ts b/src/vs/platform/contextkey/browser/contextKeyService.ts index 21320c0d..df2e5b70 100644 --- a/src/vs/platform/contextkey/browser/contextKeyService.ts +++ b/src/vs/platform/contextkey/browser/contextKeyService.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter, Event, PauseableEmitter } from 'vs/base/common/event'; +import { Emitter, PauseableEmitter } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; -import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IDisposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { TernarySearchTree } from 'vs/base/common/map'; import { distinct } from 'vs/base/common/objects'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; @@ -244,12 +244,14 @@ class CompositeContextKeyChangeEvent implements IContextKeyChangeEvent { } export abstract class AbstractContextKeyService implements IContextKeyService { - public _serviceBrand: undefined; + declare _serviceBrand: undefined; protected _isDisposed: boolean; - protected _onDidChangeContext = new PauseableEmitter({ merge: input => new CompositeContextKeyChangeEvent(input) }); protected _myContextId: number; + protected _onDidChangeContext = new PauseableEmitter({ merge: input => new CompositeContextKeyChangeEvent(input) }); + readonly onDidChangeContext = this._onDidChangeContext.event; + constructor(myContextId: number) { this._isDisposed = false; this._myContextId = myContextId; @@ -268,9 +270,6 @@ export abstract class AbstractContextKeyService implements IContextKeyService { return new ContextKey(this, key, defaultValue); } - public get onDidChangeContext(): Event { - return this._onDidChangeContext.event; - } bufferChangeEvents(callback: Function): void { this._onDidChangeContext.pause(); @@ -371,6 +370,7 @@ export class ContextKeyService extends AbstractContextKeyService implements ICon } public dispose(): void { + this._onDidChangeContext.dispose(); this._isDisposed = true; this._toDispose.dispose(); } @@ -407,34 +407,34 @@ class ScopedContextKeyService extends AbstractContextKeyService { private _parent: AbstractContextKeyService; private _domNode: IContextKeyServiceTarget | undefined; - private _parentChangeListener: IDisposable | undefined; + private readonly _parentChangeListener = new MutableDisposable(); constructor(parent: AbstractContextKeyService, domNode?: IContextKeyServiceTarget) { super(parent.createChildContext()); this._parent = parent; - this.updateParentChangeListener(); + this._updateParentChangeListener(); if (domNode) { this._domNode = domNode; if (this._domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) { - console.error('Element already has context attribute'); + let extraInfo = ''; + if ((this._domNode as HTMLElement).classList) { + extraInfo = Array.from((this._domNode as HTMLElement).classList.values()).join(', '); + } + + console.error(`Element already has context attribute${extraInfo ? ': ' + extraInfo : ''}`); } this._domNode.setAttribute(KEYBINDING_CONTEXT_ATTR, String(this._myContextId)); } } - private updateParentChangeListener(): void { - if (this._parentChangeListener) { - this._parentChangeListener.dispose(); - } - - this._parentChangeListener = this._parent.onDidChangeContext(e => { - // Forward parent events to this listener. Parent will change. - this._onDidChangeContext.fire(e); - }); + private _updateParentChangeListener(): void { + // Forward parent events to this listener. Parent will change. + this._parentChangeListener.value = this._parent.onDidChangeContext(this._onDidChangeContext.fire, this._onDidChangeContext); } public dispose(): void { + this._onDidChangeContext.dispose(); this._isDisposed = true; this._parent.disposeContext(this._myContextId); this._parentChangeListener?.dispose(); @@ -444,10 +444,6 @@ class ScopedContextKeyService extends AbstractContextKeyService { } } - public get onDidChangeContext(): Event { - return this._onDidChangeContext.event; - } - public getContextValuesContainer(contextId: number): Context { if (this._isDisposed) { return NullContext.INSTANCE; @@ -473,7 +469,7 @@ class ScopedContextKeyService extends AbstractContextKeyService { const thisContainer = this._parent.getContextValuesContainer(this._myContextId); const oldAllValues = thisContainer.collectAllValues(); this._parent = parentContextKeyService; - this.updateParentChangeListener(); + this._updateParentChangeListener(); const newParentContainer = this._parent.getContextValuesContainer(this._parent.contextId); thisContainer.updateParent(newParentContainer); diff --git a/src/vs/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index ea7c8387..fde5606e 100644 --- a/src/vs/platform/contextkey/common/contextkey.ts +++ b/src/vs/platform/contextkey/common/contextkey.ts @@ -1278,11 +1278,11 @@ export class RawContextKey extends ContextKeyDefinedExpr { return ContextKeyExpr.not(this.key); } - public isEqualTo(value: string): ContextKeyExpression { + public isEqualTo(value: any): ContextKeyExpression { return ContextKeyExpr.equals(this.key, value); } - public notEqualsTo(value: string): ContextKeyExpression { + public notEqualsTo(value: any): ContextKeyExpression { return ContextKeyExpr.notEquals(this.key, value); } } diff --git a/src/vs/platform/contextkey/test/browser/contextkey.test.ts b/src/vs/platform/contextkey/test/browser/contextkey.test.ts index d436b6cd..aca26b1d 100644 --- a/src/vs/platform/contextkey/test/browser/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/browser/contextkey.test.ts @@ -34,10 +34,10 @@ suite('ContextKeyService', () => { assert.ok(e.affectsSome(new Set(['testC'])), 'testC changed'); assert.ok(!e.affectsSome(new Set(['testD'])), 'testD did not change'); - assert.equal(child.getContextKeyValue('testA'), 3); - assert.equal(child.getContextKeyValue('testB'), undefined); - assert.equal(child.getContextKeyValue('testC'), 4); - assert.equal(child.getContextKeyValue('testD'), 0); + assert.strictEqual(child.getContextKeyValue('testA'), 3); + assert.strictEqual(child.getContextKeyValue('testB'), undefined); + assert.strictEqual(child.getContextKeyValue('testC'), 4); + assert.strictEqual(child.getContextKeyValue('testD'), 0); } catch (err) { reject(err); return; diff --git a/src/vs/platform/contextkey/test/common/contextkey.test.ts b/src/vs/platform/contextkey/test/common/contextkey.test.ts index 91a548be..1abe1a07 100644 --- a/src/vs/platform/contextkey/test/common/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/common/contextkey.test.ts @@ -67,7 +67,7 @@ suite('ContextKeyExpr', () => { function testExpression(expr: string, expected: boolean): void { // console.log(expr + ' ' + expected); let rules = ContextKeyExpr.deserialize(expr); - assert.equal(rules!.evaluate(context), expected, expr); + assert.strictEqual(rules!.evaluate(context), expected, expr); } function testBatch(expr: string, value: any): void { /* eslint-disable eqeqeq */ @@ -153,17 +153,17 @@ suite('ContextKeyExpr', () => { test('ContextKeyInExpr', () => { const ainb = ContextKeyExpr.deserialize('a in b')!; - assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [3, 2, 1] })), true); - assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2, 3] })), true); - assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2] })), false); - assert.equal(ainb.evaluate(createContext({ 'a': 3 })), false); - assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': null })), false); - assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': ['x'] })), true); - assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': ['y'] })), false); - assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': {} })), false); - assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': false } })), true); - assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': true } })), true); - assert.equal(ainb.evaluate(createContext({ 'a': 'prototype', 'b': {} })), false); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 3, 'b': [3, 2, 1] })), true); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2, 3] })), true); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2] })), false); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 3 })), false); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 3, 'b': null })), false); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': ['x'] })), true); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': ['y'] })), false); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': {} })), false); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': false } })), true); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': true } })), true); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 'prototype', 'b': {} })), false); }); test('issue #106524: distributing AND should normalize', () => { @@ -184,13 +184,13 @@ suite('ContextKeyExpr', () => { ContextKeyExpr.has('c') ) ); - assert.equal(actual!.equals(expected!), true); + assert.strictEqual(actual!.equals(expected!), true); }); test('Greater, GreaterEquals, Smaller, SmallerEquals evaluate', () => { function checkEvaluate(expr: string, ctx: any, expected: any): void { const _expr = ContextKeyExpr.deserialize(expr)!; - assert.equal(_expr.evaluate(createContext(ctx)), expected); + assert.strictEqual(_expr.evaluate(createContext(ctx)), expected); } checkEvaluate('a>1', {}, false); @@ -236,7 +236,7 @@ suite('ContextKeyExpr', () => { function checkNegate(expr: string, expected: string): void { const a = ContextKeyExpr.deserialize(expr)!; const b = a.negate(); - assert.equal(b.serialize(), expected); + assert.strictEqual(b.serialize(), expected); } checkNegate('a>1', 'a <= 1'); diff --git a/src/vs/platform/contextview/browser/contextMenuHandler.ts b/src/vs/platform/contextview/browser/contextMenuHandler.ts index 1788290c..0b515dc5 100644 --- a/src/vs/platform/contextview/browser/contextMenuHandler.ts +++ b/src/vs/platform/contextview/browser/contextMenuHandler.ts @@ -158,7 +158,7 @@ export class ContextMenuHandler { } private onDidActionRun(e: IRunEvent): void { - if (e.error && this.notificationService) { + if (e.error) { this.notificationService.error(e.error); } } diff --git a/src/vs/platform/debug/common/extensionHostDebug.ts b/src/vs/platform/debug/common/extensionHostDebug.ts index b263bdd9..b30c4e44 100644 --- a/src/vs/platform/debug/common/extensionHostDebug.ts +++ b/src/vs/platform/debug/common/extensionHostDebug.ts @@ -5,7 +5,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; -import { IRemoteConsoleLog } from 'vs/base/common/console'; import { IProcessEnvironment } from 'vs/base/common/platform'; export const IExtensionHostDebugService = createDecorator('extensionHostDebugService'); @@ -16,11 +15,6 @@ export interface IAttachSessionEvent { port: number; } -export interface ILogToSessionEvent { - sessionId: string; - log: IRemoteConsoleLog; -} - export interface ITerminateSessionEvent { sessionId: string; subId?: string; @@ -50,9 +44,6 @@ export interface IExtensionHostDebugService { attachSession(sessionId: string, port: number, subId?: string): void; readonly onAttachSession: Event; - logToSession(sessionId: string, log: IRemoteConsoleLog): void; - readonly onLogToSession: Event; - terminateSession(sessionId: string, subId?: string): void; readonly onTerminateSession: Event; diff --git a/src/vs/platform/debug/common/extensionHostDebugIpc.ts b/src/vs/platform/debug/common/extensionHostDebugIpc.ts index 60011be1..09c2a191 100644 --- a/src/vs/platform/debug/common/extensionHostDebugIpc.ts +++ b/src/vs/platform/debug/common/extensionHostDebugIpc.ts @@ -4,9 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IReloadSessionEvent, ICloseSessionEvent, IAttachSessionEvent, ILogToSessionEvent, ITerminateSessionEvent, IExtensionHostDebugService, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug'; +import { IReloadSessionEvent, ICloseSessionEvent, IAttachSessionEvent, ITerminateSessionEvent, IExtensionHostDebugService, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug'; import { Event, Emitter } from 'vs/base/common/event'; -import { IRemoteConsoleLog } from 'vs/base/common/console'; import { Disposable } from 'vs/base/common/lifecycle'; import { IProcessEnvironment } from 'vs/base/common/platform'; @@ -17,7 +16,6 @@ export class ExtensionHostDebugBroadcastChannel implements IServerChan private readonly _onCloseEmitter = new Emitter(); private readonly _onReloadEmitter = new Emitter(); private readonly _onTerminateEmitter = new Emitter(); - private readonly _onLogToEmitter = new Emitter(); private readonly _onAttachEmitter = new Emitter(); call(ctx: TContext, command: string, arg?: any): Promise { @@ -28,8 +26,6 @@ export class ExtensionHostDebugBroadcastChannel implements IServerChan return Promise.resolve(this._onReloadEmitter.fire({ sessionId: arg[0] })); case 'terminate': return Promise.resolve(this._onTerminateEmitter.fire({ sessionId: arg[0] })); - case 'log': - return Promise.resolve(this._onLogToEmitter.fire({ sessionId: arg[0], log: arg[1] })); case 'attach': return Promise.resolve(this._onAttachEmitter.fire({ sessionId: arg[0], port: arg[1], subId: arg[2] })); } @@ -44,8 +40,6 @@ export class ExtensionHostDebugBroadcastChannel implements IServerChan return this._onReloadEmitter.event; case 'terminate': return this._onTerminateEmitter.event; - case 'log': - return this._onLogToEmitter.event; case 'attach': return this._onAttachEmitter.event; } @@ -85,14 +79,6 @@ export class ExtensionHostDebugChannelClient extends Disposable implements IExte return this.channel.listen('attach'); } - logToSession(sessionId: string, log: IRemoteConsoleLog): void { - this.channel.call('log', [sessionId, log]); - } - - get onLogToSession(): Event { - return this.channel.listen('log'); - } - terminateSession(sessionId: string, subId?: string): void { this.channel.call('terminate', [sessionId, subId]); } diff --git a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts index d05bd7ca..f0ac255c 100644 --- a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts +++ b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts @@ -8,8 +8,7 @@ import { IProcessEnvironment } from 'vs/base/common/platform'; import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; import { createServer, AddressInfo } from 'net'; import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; -import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -import { OpenContext } from 'vs/platform/windows/node/window'; +import { IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows'; export class ElectronExtensionHostDebugBroadcastChannel extends ExtensionHostDebugBroadcastChannel { diff --git a/src/vs/platform/diagnostics/node/diagnosticsService.ts b/src/vs/platform/diagnostics/node/diagnosticsService.ts index 2fe576f1..2f271144 100644 --- a/src/vs/platform/diagnostics/node/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/node/diagnosticsService.ts @@ -314,10 +314,10 @@ export class DiagnosticsService implements IDiagnosticsService { if (isLinux) { systemInfo.linuxEnv = { - desktopSession: process.env.DESKTOP_SESSION, - xdgSessionDesktop: process.env.XDG_SESSION_DESKTOP, - xdgCurrentDesktop: process.env.XDG_CURRENT_DESKTOP, - xdgSessionType: process.env.XDG_SESSION_TYPE + desktopSession: process.env['DESKTOP_SESSION'], + xdgSessionDesktop: process.env['XDG_SESSION_DESKTOP'], + xdgCurrentDesktop: process.env['XDG_CURRENT_DESKTOP'], + xdgSessionType: process.env['XDG_SESSION_TYPE'] }; } diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index 8f7bb194..25e49ef1 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -181,6 +181,7 @@ export interface IDialogOptions { cancelId?: number; detail?: string; checkbox?: ICheckbox; + useCustom?: boolean; } export interface IInput { diff --git a/src/vs/platform/dialogs/electron-main/dialogs.ts b/src/vs/platform/dialogs/electron-main/dialogMainService.ts similarity index 100% rename from src/vs/platform/dialogs/electron-main/dialogs.ts rename to src/vs/platform/dialogs/electron-main/dialogMainService.ts diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index e719e129..019410e6 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -28,6 +28,7 @@ export interface NativeParsedArgs { 'prof-startup'?: boolean; 'prof-startup-prefix'?: string; 'prof-append-timers'?: string; + 'prof-v8-extensions'?: boolean; verbose?: boolean; trace?: boolean; 'trace-category-filter'?: string; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 7263bcbf..7b6a60b5 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -53,8 +53,8 @@ export const OPTIONS: OptionDescriptions> = { 'extensions-download-dir': { type: 'string' }, 'builtin-extensions-dir': { type: 'string' }, 'list-extensions': { type: 'boolean', cat: 'e', description: localize('listExtensions', "List the installed extensions.") }, - 'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extension.") }, - 'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extension.") }, + 'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extensions.") }, + 'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extensions.") }, 'install-extension': { type: 'string[]', cat: 'e', args: 'extension-id[@version] | path-to-vsix', description: localize('installExtension', "Installs or updates the extension. The identifier of an extension is always `${publisher}.${name}`. Use `--force` argument to update to latest version. To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.") }, 'uninstall-extension': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('uninstallExtension', "Uninstalls an extension.") }, 'enable-proposed-api': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('experimentalApis', "Enables proposed API features for extensions. Can receive one or more extension IDs to enable individually.") }, @@ -66,6 +66,7 @@ export const OPTIONS: OptionDescriptions> = { 'prof-startup': { type: 'boolean', cat: 't', description: localize('prof-startup', "Run CPU profiler during startup") }, 'prof-append-timers': { type: 'string' }, 'prof-startup-prefix': { type: 'string' }, + 'prof-v8-extensions': { type: 'boolean' }, 'disable-extensions': { type: 'boolean', deprecates: 'disableExtensions', cat: 't', description: localize('disableExtensions', "Disable all installed extensions.") }, 'disable-extension': { type: 'string[]', cat: 't', args: 'extension-id', description: localize('disableExtension', "Disable an extension.") }, 'sync': { type: 'string', cat: 't', description: localize('turn sync', "Turn sync on or off"), args: ['on', 'off'] }, @@ -149,10 +150,6 @@ export function parseArgs(args: string[], options: OptionDescriptions, err const string: string[] = []; const boolean: string[] = []; for (let optionId in options) { - if (optionId[0] === '_') { - continue; - } - const o = options[optionId]; if (o.alias) { alias[optionId] = o.alias; diff --git a/src/vs/platform/environment/test/node/environmentService.test.ts b/src/vs/platform/environment/test/node/environmentService.test.ts index d30dc8a6..ca33c226 100644 --- a/src/vs/platform/environment/test/node/environmentService.test.ts +++ b/src/vs/platform/environment/test/node/environmentService.test.ts @@ -13,35 +13,35 @@ suite('EnvironmentService', () => { test('parseExtensionHostPort when built', () => { const parse = (a: string[]) => parseExtensionHostPort(parseArgs(a, OPTIONS), true); - assert.deepEqual(parse([]), { port: null, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugPluginHost']), { port: null, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugBrkPluginHost']), { port: null, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, debugId: undefined }); - assert.deepEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); + assert.deepStrictEqual(parse([]), { port: null, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost']), { port: null, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugBrkPluginHost']), { port: null, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); - assert.deepEqual(parse(['--inspect-extensions']), { port: null, break: false, debugId: undefined }); - assert.deepEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, debugId: undefined }); - assert.deepEqual(parse(['--inspect-brk-extensions']), { port: null, break: false, debugId: undefined }); - assert.deepEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, debugId: undefined }); - assert.deepEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); + assert.deepStrictEqual(parse(['--inspect-extensions']), { port: null, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-brk-extensions']), { port: null, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); }); test('parseExtensionHostPort when unbuilt', () => { const parse = (a: string[]) => parseExtensionHostPort(parseArgs(a, OPTIONS), false); - assert.deepEqual(parse([]), { port: 5870, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugPluginHost']), { port: 5870, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugBrkPluginHost']), { port: 5870, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, debugId: undefined }); - assert.deepEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); + assert.deepStrictEqual(parse([]), { port: 5870, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost']), { port: 5870, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugBrkPluginHost']), { port: 5870, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); - assert.deepEqual(parse(['--inspect-extensions']), { port: 5870, break: false, debugId: undefined }); - assert.deepEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, debugId: undefined }); - assert.deepEqual(parse(['--inspect-brk-extensions']), { port: 5870, break: false, debugId: undefined }); - assert.deepEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, debugId: undefined }); - assert.deepEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); + assert.deepStrictEqual(parse(['--inspect-extensions']), { port: 5870, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-brk-extensions']), { port: 5870, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); }); test('userDataPath', () => { @@ -57,10 +57,10 @@ suite('EnvironmentService', () => { test('careful with boolean file names', function () { let actual = parseArgs(['-r', 'arg.txt'], OPTIONS); assert(actual['reuse-window']); - assert.deepEqual(actual._, ['arg.txt']); + assert.deepStrictEqual(actual._, ['arg.txt']); actual = parseArgs(['-r', 'true.txt'], OPTIONS); assert(actual['reuse-window']); - assert.deepEqual(actual._, ['true.txt']); + assert.deepStrictEqual(actual._, ['true.txt']); }); }); diff --git a/src/vs/platform/environment/test/node/nativeModules.test.ts b/src/vs/platform/environment/test/node/nativeModules.test.ts index b64282f6..28013dfc 100644 --- a/src/vs/platform/environment/test/node/nativeModules.test.ts +++ b/src/vs/platform/environment/test/node/nativeModules.test.ts @@ -37,11 +37,6 @@ suite('Native Modules (all platforms)', () => { assert.ok(typeof spdlog.createRotatingLogger === 'function', testErrorMessage('spdlog')); }); - test('v8-inspect-profiler', async () => { - const profiler = await import('v8-inspect-profiler'); - assert.ok(typeof profiler.startProfiling === 'function', testErrorMessage('v8-inspect-profiler')); - }); - test('vscode-nsfw', async () => { const nsfWatcher = await import('vscode-nsfw'); assert.ok(typeof nsfWatcher === 'function', testErrorMessage('vscode-nsfw')); @@ -90,7 +85,7 @@ suite('Native Modules (all platforms)', () => { test('vscode-windows-ca-certs', async () => { // @ts-ignore Windows only const windowsCerts = await import('vscode-windows-ca-certs'); - const store = windowsCerts(); + const store = new windowsCerts.Crypt32(); assert.ok(windowsCerts, testErrorMessage('vscode-windows-ca-certs')); let certCount = 0; try { diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 3f0dd835..45d3b68c 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -9,7 +9,7 @@ import { getGalleryExtensionId, getGalleryExtensionTelemetryData, adoptToGallery import { getOrDefault } from 'vs/base/common/objects'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IPager } from 'vs/base/common/paging'; -import { IRequestService, asJson, asText } from 'vs/platform/request/common/request'; +import { IRequestService, asJson, asText, isSuccess } from 'vs/platform/request/common/request'; import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -147,6 +147,35 @@ const DefaultQueryState: IQueryState = { assetTypes: [] }; +type GalleryServiceQueryClassification = { + filterTypes: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + sortBy: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + sortOrder: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + duration: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', 'isMeasurement': true }; + success: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + requestBodySize: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + responseBodySize?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + statusCode?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + errorCode?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + count?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; +}; + +type QueryTelemetryData = { + filterTypes: string[]; + sortBy: string; + sortOrder: string; +}; + +type GalleryServiceQueryEvent = QueryTelemetryData & { + duration: number; + success: boolean; + requestBodySize: string; + responseBodySize?: string; + statusCode?: string; + errorCode?: string; + count?: string; +}; + class Query { constructor(private state = DefaultQueryState) { } @@ -196,6 +225,14 @@ class Query { const criterium = this.state.criteria.filter(criterium => criterium.filterType === FilterType.SearchText)[0]; return criterium && criterium.value ? criterium.value : ''; } + + get telemetryData(): QueryTelemetryData { + return { + filterTypes: this.state.criteria.map(criterium => String(criterium.filterType)), + sortBy: String(this.sortBy), + sortOrder: String(this.sortOrder) + }; + } } function getStatistic(statistics: IRawGalleryExtensionStatistics[], name: string): number { @@ -447,20 +484,9 @@ export class ExtensionGalleryService implements IExtensionGalleryService { throw new Error('No extension gallery service configured.'); } - const type = options.names ? 'ids' : (options.text ? 'text' : 'all'); let text = options.text || ''; const pageSize = getOrDefault(options, o => o.pageSize, 50); - type GalleryServiceQueryClassification = { - type: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - text: { classification: 'CustomerContent', purpose: 'FeatureInsight' }; - }; - type GalleryServiceQueryEvent = { - type: string; - text: string; - }; - this.telemetryService.publicLog2('galleryService:query', { type, text }); - let query = new Query() .withFlags(Flags.IncludeLatestVersionOnly, Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeFiles, Flags.IncludeVersionProperties) .withPage(1, pageSize) @@ -543,27 +569,49 @@ export class ExtensionGalleryService implements IExtensionGalleryService { 'Content-Length': String(data.length) }; - const context = await this.requestService.request({ - type: 'POST', - url: this.api('/extensionquery'), - data, - headers - }, token); + const startTime = new Date().getTime(); + let context: IRequestContext | undefined, error: any, total: number = 0; - if (context.res.statusCode && context.res.statusCode >= 400 && context.res.statusCode < 500) { - return { galleryExtensions: [], total: 0 }; + try { + context = await this.requestService.request({ + type: 'POST', + url: this.api('/extensionquery'), + data, + headers + }, token); + + if (context.res.statusCode && context.res.statusCode >= 400 && context.res.statusCode < 500) { + return { galleryExtensions: [], total }; + } + + const result = await asJson(context); + if (result) { + const r = result.results[0]; + const galleryExtensions = r.extensions; + const resultCount = r.resultMetadata && r.resultMetadata.filter(m => m.metadataType === 'ResultCount')[0]; + total = resultCount && resultCount.metadataItems.filter(i => i.name === 'TotalCount')[0].count || 0; + + return { galleryExtensions, total }; + } + return { galleryExtensions: [], total }; + + } catch (e) { + error = e; + throw e; + } finally { + this.telemetryService.publicLog2('galleryService:query', { + ...query.telemetryData, + requestBodySize: String(data.length), + duration: new Date().getTime() - startTime, + success: !!context && isSuccess(context), + responseBodySize: context?.res.headers['Content-Length'], + statusCode: context ? String(context.res.statusCode) : undefined, + errorCode: error + ? isPromiseCanceledError(error) ? 'canceled' : getErrorMessage(error).startsWith('XHR timeout') ? 'timeout' : 'failed' + : undefined, + count: String(total) + }); } - - const result = await asJson(context); - if (result) { - const r = result.results[0]; - const galleryExtensions = r.extensions; - const resultCount = r.resultMetadata && r.resultMetadata.filter(m => m.metadataType === 'ResultCount')[0]; - const total = resultCount && resultCount.metadataItems.filter(i => i.name === 'TotalCount')[0].count || 0; - - return { galleryExtensions, total }; - } - return { galleryExtensions: [], total: 0 }; } async reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 0e46a92d..50531800 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -278,3 +278,19 @@ export const ExtensionsLocalizedLabel = { value: ExtensionsLabel, original: 'Ext export const ExtensionsChannelId = 'extensions'; export const PreferencesLabel = localize('preferences', "Preferences"); export const PreferencesLocalizedLabel = { value: PreferencesLabel, original: 'Preferences' }; + + +export interface CLIOutput { + log(s: string): void; + error(s: string): void; +} + +export const IExtensionManagementCLIService = createDecorator('IExtensionManagementCLIService'); +export interface IExtensionManagementCLIService { + readonly _serviceBrand: undefined; + + listExtensions(showVersions: boolean, category?: string, output?: CLIOutput): Promise; + installExtensions(extensions: (string | URI)[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean, output?: CLIOutput): Promise; + uninstallExtensions(extensions: (string | URI)[], force: boolean, output?: CLIOutput): Promise; + locateExtension(extensions: string[], output?: CLIOutput): Promise; +} diff --git a/src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts b/src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts new file mode 100644 index 00000000..532a550c --- /dev/null +++ b/src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts @@ -0,0 +1,347 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; + +import { isPromiseCanceledError } from 'vs/base/common/errors'; +import { URI } from 'vs/base/common/uri'; +import { gt } from 'vs/base/common/semver/semver'; +import { CLIOutput, IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { ExtensionType, EXTENSION_CATEGORIES, IExtensionManifest, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; +import { getBaseLabel } from 'vs/base/common/labels'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Schemas } from 'vs/base/common/network'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; + +const notFound = (id: string) => localize('notFound', "Extension '{0}' not found.", id); +const useId = localize('useId', "Make sure you use the full extension ID, including the publisher, e.g.: {0}", 'ms-dotnettools.csharp'); + + +function getId(manifest: IExtensionManifest, withVersion?: boolean): string { + if (withVersion) { + return `${manifest.publisher}.${manifest.name}@${manifest.version}`; + } else { + return `${manifest.publisher}.${manifest.name}`; + } +} + +const EXTENSION_ID_REGEX = /^([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/; + +export function getIdAndVersion(id: string): [string, string | undefined] { + const matches = EXTENSION_ID_REGEX.exec(id); + if (matches && matches[1]) { + return [adoptToGalleryExtensionId(matches[1]), matches[2]]; + } + return [adoptToGalleryExtensionId(id), undefined]; +} + +type InstallExtensionInfo = { id: string, version?: string, installOptions: InstallOptions }; + + +export class ExtensionManagementCLIService implements IExtensionManagementCLIService { + + _serviceBrand: any; + + constructor( + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, + @ILocalizationsService private readonly localizationsService: ILocalizationsService + ) { } + + protected get location(): string | undefined { + return undefined; + } + + public async listExtensions(showVersions: boolean, category?: string, output: CLIOutput = console): Promise { + let extensions = await this.extensionManagementService.getInstalled(ExtensionType.User); + const categories = EXTENSION_CATEGORIES.map(c => c.toLowerCase()); + if (category && category !== '') { + if (categories.indexOf(category.toLowerCase()) < 0) { + output.log('Invalid category please enter a valid category. To list valid categories run --category without a category specified'); + return; + } + extensions = extensions.filter(e => { + if (e.manifest.categories) { + const lowerCaseCategories: string[] = e.manifest.categories.map(c => c.toLowerCase()); + return lowerCaseCategories.indexOf(category.toLowerCase()) > -1; + } + return false; + }); + } else if (category === '') { + output.log('Possible Categories: '); + categories.forEach(category => { + output.log(category); + }); + return; + } + if (this.location) { + output.log(localize('listFromLocation', "Extensions installed on {0}:", this.location)); + } + + extensions = extensions.sort((e1, e2) => e1.identifier.id.localeCompare(e2.identifier.id)); + let lastId: string | undefined = undefined; + for (let extension of extensions) { + if (lastId !== extension.identifier.id) { + lastId = extension.identifier.id; + output.log(getId(extension.manifest, showVersions)); + } + } + } + + public async installExtensions(extensions: (string | URI)[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean, output: CLIOutput = console): Promise { + const failed: string[] = []; + const installedExtensionsManifests: IExtensionManifest[] = []; + if (extensions.length) { + output.log(this.location ? localize('installingExtensionsOnLocation', "Installing extensions on {0}...", this.location) : localize('installingExtensions', "Installing extensions...")); + } + + const installed = await this.extensionManagementService.getInstalled(ExtensionType.User); + const checkIfNotInstalled = (id: string, version?: string): boolean => { + const installedExtension = installed.find(i => areSameExtensions(i.identifier, { id })); + if (installedExtension) { + if (!version && !force) { + output.log(localize('alreadyInstalled-checkAndUpdate', "Extension '{0}' v{1} is already installed. Use '--force' option to update to latest version or provide '@' to install a specific version, for example: '{2}@1.2.3'.", id, installedExtension.manifest.version, id)); + return false; + } + if (version && installedExtension.manifest.version === version) { + output.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", `${id}@${version}`)); + return false; + } + } + return true; + }; + const vsixs: URI[] = []; + const installExtensionInfos: InstallExtensionInfo[] = []; + for (const extension of extensions) { + if (extension instanceof URI) { + vsixs.push(extension); + } else { + const [id, version] = getIdAndVersion(extension); + if (checkIfNotInstalled(id, version)) { + installExtensionInfos.push({ id, version, installOptions: { isBuiltin: false, isMachineScoped } }); + } + } + } + for (const extension of builtinExtensionIds) { + const [id, version] = getIdAndVersion(extension); + if (checkIfNotInstalled(id, version)) { + installExtensionInfos.push({ id, version, installOptions: { isBuiltin: true, isMachineScoped: false } }); + } + } + + if (vsixs.length) { + await Promise.all(vsixs.map(async vsix => { + try { + const manifest = await this.installVSIX(vsix, force, output); + if (manifest) { + installedExtensionsManifests.push(manifest); + } + } catch (err) { + output.error(err.message || err.stack || err); + failed.push(vsix.toString()); + } + })); + } + + if (installExtensionInfos.length) { + + const galleryExtensions = await this.getGalleryExtensions(installExtensionInfos); + + await Promise.all(installExtensionInfos.map(async extensionInfo => { + const gallery = galleryExtensions.get(extensionInfo.id.toLowerCase()); + if (gallery) { + try { + const manifest = await this.installFromGallery(extensionInfo, gallery, installed, force, output); + if (manifest) { + installedExtensionsManifests.push(manifest); + } + } catch (err) { + output.error(err.message || err.stack || err); + failed.push(extensionInfo.id); + } + } else { + output.error(`${notFound(extensionInfo.version ? `${extensionInfo.id}@${extensionInfo.version}` : extensionInfo.id)}\n${useId}`); + failed.push(extensionInfo.id); + } + })); + + } + + if (installedExtensionsManifests.some(manifest => isLanguagePackExtension(manifest))) { + await this.updateLocalizationsCache(); + } + + if (failed.length) { + throw new Error(localize('installation failed', "Failed Installing Extensions: {0}", failed.join(', '))); + } + } + + private async installVSIX(vsix: URI, force: boolean, output: CLIOutput): Promise { + + const manifest = await this.extensionManagementService.getManifest(vsix); + if (!manifest) { + throw new Error('Invalid vsix'); + } + + const valid = await this.validateVSIX(manifest, force, output); + if (valid) { + try { + await this.extensionManagementService.install(vsix); + output.log(localize('successVsixInstall', "Extension '{0}' was successfully installed.", getBaseLabel(vsix))); + return manifest; + } catch (error) { + if (isPromiseCanceledError(error)) { + output.log(localize('cancelVsixInstall', "Cancelled installing extension '{0}'.", getBaseLabel(vsix))); + return null; + } else { + throw error; + } + } + } + return null; + } + + private async getGalleryExtensions(extensions: InstallExtensionInfo[]): Promise> { + const extensionIds = extensions.filter(({ version }) => version === undefined).map(({ id }) => id); + const extensionsWithIdAndVersion = extensions.filter(({ version }) => version !== undefined); + + const galleryExtensions = new Map(); + await Promise.all([ + (async () => { + const result = await this.extensionGalleryService.getExtensions(extensionIds, CancellationToken.None); + result.forEach(extension => galleryExtensions.set(extension.identifier.id.toLowerCase(), extension)); + })(), + Promise.all(extensionsWithIdAndVersion.map(async ({ id, version }) => { + const extension = await this.extensionGalleryService.getCompatibleExtension({ id }, version); + if (extension) { + galleryExtensions.set(extension.identifier.id.toLowerCase(), extension); + } + })) + ]); + + return galleryExtensions; + } + + private async installFromGallery({ id, version, installOptions }: InstallExtensionInfo, galleryExtension: IGalleryExtension, installed: ILocalExtension[], force: boolean, output: CLIOutput): Promise { + const manifest = await this.extensionGalleryService.getManifest(galleryExtension, CancellationToken.None); + if (manifest && !this.validateExtensionKind(manifest, output)) { + return null; + } + + const installedExtension = installed.find(e => areSameExtensions(e.identifier, galleryExtension.identifier)); + if (installedExtension) { + if (galleryExtension.version === installedExtension.manifest.version) { + output.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", version ? `${id}@${version}` : id)); + return null; + } + output.log(localize('updateMessage', "Updating the extension '{0}' to the version {1}", id, galleryExtension.version)); + } + + try { + if (installOptions.isBuiltin) { + output.log(localize('installing builtin ', "Installing builtin extension '{0}' v{1}...", id, galleryExtension.version)); + } else { + output.log(localize('installing', "Installing extension '{0}' v{1}...", id, galleryExtension.version)); + } + + await this.extensionManagementService.installFromGallery(galleryExtension, installOptions); + output.log(localize('successInstall', "Extension '{0}' v{1} was successfully installed.", id, galleryExtension.version)); + return manifest; + } catch (error) { + if (isPromiseCanceledError(error)) { + output.log(localize('cancelInstall', "Cancelled installing extension '{0}'.", id)); + return null; + } else { + throw error; + } + } + } + + protected validateExtensionKind(_manifest: IExtensionManifest, output: CLIOutput): boolean { + return true; + } + + private async validateVSIX(manifest: IExtensionManifest, force: boolean, output: CLIOutput): Promise { + const extensionIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; + const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User); + const newer = installedExtensions.find(local => areSameExtensions(extensionIdentifier, local.identifier) && gt(local.manifest.version, manifest.version)); + + if (newer && !force) { + output.log(localize('forceDowngrade', "A newer version of extension '{0}' v{1} is already installed. Use '--force' option to downgrade to older version.", newer.identifier.id, newer.manifest.version, manifest.version)); + return false; + } + + return this.validateExtensionKind(manifest, output); + } + + public async uninstallExtensions(extensions: (string | URI)[], force: boolean, output: CLIOutput = console): Promise { + const getExtensionId = async (extensionDescription: string | URI): Promise => { + if (extensionDescription instanceof URI) { + const manifest = await this.extensionManagementService.getManifest(extensionDescription); + return getId(manifest); + } + return extensionDescription; + }; + + const uninstalledExtensions: ILocalExtension[] = []; + for (const extension of extensions) { + const id = await getExtensionId(extension); + const installed = await this.extensionManagementService.getInstalled(); + const extensionsToUninstall = installed.filter(e => areSameExtensions(e.identifier, { id })); + if (!extensionsToUninstall.length) { + throw new Error(`${this.notInstalled(id)}\n${useId}`); + } + if (extensionsToUninstall.some(e => e.type === ExtensionType.System)) { + output.log(localize('builtin', "Extension '{0}' is a Built-in extension and cannot be uninstalled", id)); + return; + } + if (!force && extensionsToUninstall.some(e => e.isBuiltin)) { + output.log(localize('forceUninstall', "Extension '{0}' is marked as a Built-in extension by user. Please use '--force' option to uninstall it.", id)); + return; + } + output.log(localize('uninstalling', "Uninstalling {0}...", id)); + for (const extensionToUninstall of extensionsToUninstall) { + await this.extensionManagementService.uninstall(extensionToUninstall); + uninstalledExtensions.push(extensionToUninstall); + } + + if (this.location) { + output.log(localize('successUninstallFromLocation', "Extension '{0}' was successfully uninstalled from {1}!", id, this.location)); + } else { + output.log(localize('successUninstall', "Extension '{0}' was successfully uninstalled!", id)); + } + + } + + if (uninstalledExtensions.some(e => isLanguagePackExtension(e.manifest))) { + await this.updateLocalizationsCache(); + } + } + + public async locateExtension(extensions: string[], output: CLIOutput = console): Promise { + const installed = await this.extensionManagementService.getInstalled(); + extensions.forEach(e => { + installed.forEach(i => { + if (i.identifier.id === e) { + if (i.location.scheme === Schemas.file) { + output.log(i.location.fsPath); + return; + } + } + }); + }); + } + + + private updateLocalizationsCache(): Promise { + return this.localizationsService.update(); + } + + private notInstalled(id: string) { + return this.location ? localize('notInstalleddOnLocation', "Extension '{0}' is not installed on {1}.", id, this.location) : localize('notInstalled', "Extension '{0}' is not installed.", id); + } + +} diff --git a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts index 20d24771..39374557 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -42,12 +42,16 @@ export class ExtensionIdentifierWithVersion implements IExtensionIdentifierWithV } } +export function getExtensionId(publisher: string, name: string): string { + return `${publisher}.${name}`; +} + export function adoptToGalleryExtensionId(id: string): string { return id.toLocaleLowerCase(); } export function getGalleryExtensionId(publisher: string, name: string): string { - return `${publisher.toLocaleLowerCase()}.${name.toLocaleLowerCase()}`; + return adoptToGalleryExtensionId(getExtensionId(publisher, name)); } export function groupByExtension(extensions: T[], getExtensionIdentifier: (t: T) => IExtensionIdentifier): T[][] { diff --git a/src/vs/platform/extensionManagement/electron-sandbox/extensionTipsService.ts b/src/vs/platform/extensionManagement/electron-sandbox/extensionTipsService.ts index 7d17d9ef..a9ec2f35 100644 --- a/src/vs/platform/extensionManagement/electron-sandbox/extensionTipsService.ts +++ b/src/vs/platform/extensionManagement/electron-sandbox/extensionTipsService.ts @@ -21,6 +21,8 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { localize } from 'vs/nls'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { Event } from 'vs/base/common/event'; +import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; type ExeExtensionRecommendationsClassification = { extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' }; @@ -52,6 +54,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService { @ITelemetryService private readonly telemetryService: ITelemetryService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IStorageService private readonly storageService: IStorageService, + @INativeHostService private readonly nativeHostService: INativeHostService, @IExtensionRecommendationNotificationService private readonly extensionRecommendationNotificationService: IExtensionRecommendationNotificationService, @IFileService fileService: IFileService, @IProductService productService: IProductService, @@ -172,6 +175,11 @@ export class ExtensionTipsService extends BaseExtensionTipsService { case RecommendationsNotificationResult.Ignored: this.highImportanceTipsByExe.delete(exeName); break; + case RecommendationsNotificationResult.IncompatibleWindow: + // Recommended in incompatible window. Schedule the prompt after active window change + const onActiveWindowChange = Event.once(Event.latch(Event.any(this.nativeHostService.onDidOpenWindow, this.nativeHostService.onDidFocusWindow))); + this._register(onActiveWindowChange(() => this.promptHighImportanceExeBasedTip())); + break; case RecommendationsNotificationResult.TooMany: // Too many notifications. Schedule the prompt after one hour const disposable = this._register(disposableTimeout(() => { disposable.dispose(); this.promptHighImportanceExeBasedTip(); }, 60 * 60 * 1000 /* 1 hour */)); @@ -217,6 +225,12 @@ export class ExtensionTipsService extends BaseExtensionTipsService { this.promptMediumImportanceExeBasedTip(); break; + case RecommendationsNotificationResult.IncompatibleWindow: + // Recommended in incompatible window. Schedule the prompt after active window change + const onActiveWindowChange = Event.once(Event.latch(Event.any(this.nativeHostService.onDidOpenWindow, this.nativeHostService.onDidFocusWindow))); + this._register(onActiveWindowChange(() => this.promptMediumImportanceExeBasedTip())); + break; + case RecommendationsNotificationResult.TooMany: // Too many notifications. Schedule the prompt after one hour const disposable2 = this._register(disposableTimeout(() => { disposable2.dispose(); this.promptMediumImportanceExeBasedTip(); }, 60 * 60 * 1000 /* 1 hour */)); diff --git a/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts new file mode 100644 index 00000000..c1085576 --- /dev/null +++ b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/common/extensionGalleryService'; +import { isUUID } from 'vs/base/common/uuid'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { NullLogService } from 'vs/platform/log/common/log'; +import product from 'vs/platform/product/common/product'; +import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/common/storage'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { URI } from 'vs/base/common/uri'; +import { joinPath } from 'vs/base/common/resources'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { mock } from 'vs/base/test/common/mock'; + +class EnvironmentServiceMock extends mock() { + constructor(readonly serviceMachineIdResource: URI) { + super(); + } +} + +suite('Extension Gallery Service', () => { + const disposables: DisposableStore = new DisposableStore(); + let fileService: IFileService, environmentService: IEnvironmentService, storageService: IStorageService; + + setup(() => { + const serviceMachineIdResource = joinPath(URI.file('tests').with({ scheme: 'vscode-tests' }), 'machineid'); + environmentService = new EnvironmentServiceMock(serviceMachineIdResource); + fileService = disposables.add(new FileService(new NullLogService())); + const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + fileService.registerProvider(serviceMachineIdResource.scheme, fileSystemProvider); + storageService = new InMemoryStorageService(); + }); + + teardown(() => disposables.clear()); + + test('marketplace machine id', async () => { + const headers = await resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService); + assert.ok(isUUID(headers['X-Market-User-Id'])); + const headers2 = await resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService); + assert.equal(headers['X-Market-User-Id'], headers2['X-Market-User-Id']); + }); +}); diff --git a/src/vs/platform/extensionManagement/test/node/extensionGalleryService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionGalleryService.test.ts deleted file mode 100644 index e2a7ed43..00000000 --- a/src/vs/platform/extensionManagement/test/node/extensionGalleryService.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import * as os from 'os'; -import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; -import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; -import { getRandomTestPath } from 'vs/base/test/node/testUtils'; -import { join } from 'vs/base/common/path'; -import { mkdirp, RimRafMode, rimraf } from 'vs/base/node/pfs'; -import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/common/extensionGalleryService'; -import { isUUID } from 'vs/base/common/uuid'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { IFileService } from 'vs/platform/files/common/files'; -import { FileService } from 'vs/platform/files/common/fileService'; -import { NullLogService } from 'vs/platform/log/common/log'; -import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; -import { Schemas } from 'vs/base/common/network'; -import product from 'vs/platform/product/common/product'; -import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; -import { IStorageService } from 'vs/platform/storage/common/storage'; - -suite('Extension Gallery Service', () => { - const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'extensiongalleryservice'); - const marketplaceHome = join(parentDir, 'Marketplace'); - let fileService: IFileService; - let disposables: DisposableStore; - - setup(done => { - - disposables = new DisposableStore(); - fileService = new FileService(new NullLogService()); - disposables.add(fileService); - - const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService()); - disposables.add(diskFileSystemProvider); - fileService.registerProvider(Schemas.file, diskFileSystemProvider); - - // Delete any existing backups completely and then re-create it. - rimraf(marketplaceHome, RimRafMode.MOVE).then(() => { - mkdirp(marketplaceHome).then(() => { - done(); - }, error => done(error)); - }, error => done(error)); - }); - - teardown(done => { - disposables.clear(); - rimraf(marketplaceHome, RimRafMode.MOVE).then(done, done); - }); - - test('marketplace machine id', () => { - const args = ['--user-data-dir', marketplaceHome]; - const environmentService = new NativeEnvironmentService(parseArgs(args, OPTIONS)); - const storageService: IStorageService = new TestStorageService(); - - return resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService).then(headers => { - assert.ok(isUUID(headers['X-Market-User-Id'])); - - return resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService).then(headers2 => { - assert.equal(headers['X-Market-User-Id'], headers2['X-Market-User-Id']); - }); - }); - }); -}); diff --git a/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts b/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts index cea1c09e..c3eff4b7 100644 --- a/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts +++ b/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts @@ -11,10 +11,19 @@ export const enum RecommendationSource { EXE = 3 } +export function RecommendationSourceToString(source: RecommendationSource) { + switch (source) { + case RecommendationSource.FILE: return 'file'; + case RecommendationSource.WORKSPACE: return 'workspace'; + case RecommendationSource.EXE: return 'exe'; + } +} + export const enum RecommendationsNotificationResult { Ignored = 'ignored', Cancelled = 'cancelled', TooMany = 'toomany', + IncompatibleWindow = 'incompatibleWindow', Accepted = 'reacted', } diff --git a/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts b/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts index b5bdc623..c863c64b 100644 --- a/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts +++ b/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts @@ -8,14 +8,24 @@ import { IFileSystemProviderWithFileReadWriteCapability, FileSystemProviderCapab import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { VSBuffer } from 'vs/base/common/buffer'; -import { joinPath, extUri, dirname } from 'vs/base/common/resources'; +import { Throttler } from 'vs/base/common/async'; import { localize } from 'vs/nls'; import * as browser from 'vs/base/browser/browser'; +import { joinPath } from 'vs/base/common/resources'; const INDEXEDDB_VSCODE_DB = 'vscode-web-db'; export const INDEXEDDB_USERDATA_OBJECT_STORE = 'vscode-userdata-store'; export const INDEXEDDB_LOGS_OBJECT_STORE = 'vscode-logs-store'; +// Standard FS Errors (expected to be thrown in production when invalid FS operations are requested) +const ERR_FILE_NOT_FOUND = createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound); +const ERR_FILE_IS_DIR = createFileSystemProviderError(localize('fileIsDirectory', "File is Directory"), FileSystemProviderErrorCode.FileIsADirectory); +const ERR_FILE_NOT_DIR = createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory); +const ERR_DIR_NOT_EMPTY = createFileSystemProviderError(localize('dirIsNotEmpty', "Directory is not empty"), FileSystemProviderErrorCode.Unknown); + +// Arbitrary Internal Errors (should never be thrown in production) +const ERR_UNKNOWN_INTERNAL = (message: string) => createFileSystemProviderError(localize('internal', "Internal error occured in IndexedDB File System Provider. ({0})", message), FileSystemProviderErrorCode.Unknown); + export class IndexedDB { private indexedDBPromise: Promise; @@ -38,7 +48,7 @@ export class IndexedDB { } private openIndexedDB(name: string, version: number, stores: string[]): Promise { - if (browser.isEdge) { + if (browser.isEdgeLegacy) { return Promise.resolve(null); } return new Promise((c, e) => { @@ -65,13 +75,140 @@ export class IndexedDB { }; }); } - } export interface IIndexedDBFileSystemProvider extends Disposable, IFileSystemProviderWithFileReadWriteCapability { reset(): Promise; } +type DirEntry = [string, FileType]; + +type IndexedDBFileSystemEntry = + | { + path: string, + type: FileType.Directory, + children: Map, + } + | { + path: string, + type: FileType.File, + size: number | undefined, + }; + +class IndexedDBFileSystemNode { + public type: FileType; + + constructor(private entry: IndexedDBFileSystemEntry) { + this.type = entry.type; + } + + + read(path: string) { + return this.doRead(path.split('/').filter(p => p.length)); + } + + private doRead(pathParts: string[]): IndexedDBFileSystemEntry | undefined { + if (pathParts.length === 0) { return this.entry; } + if (this.entry.type !== FileType.Directory) { + throw ERR_UNKNOWN_INTERNAL('Internal error reading from IndexedDBFSNode -- expected directory at ' + this.entry.path); + } + const next = this.entry.children.get(pathParts[0]); + + if (!next) { return undefined; } + return next.doRead(pathParts.slice(1)); + } + + delete(path: string) { + const toDelete = path.split('/').filter(p => p.length); + if (toDelete.length === 0) { + if (this.entry.type !== FileType.Directory) { + throw ERR_UNKNOWN_INTERNAL(`Internal error deleting from IndexedDBFSNode. Expected root entry to be directory`); + } + this.entry.children.clear(); + } else { + return this.doDelete(toDelete, path); + } + } + + private doDelete = (pathParts: string[], originalPath: string) => { + if (pathParts.length === 0) { + throw ERR_UNKNOWN_INTERNAL(`Internal error deleting from IndexedDBFSNode -- got no deletion path parts (encountered while deleting ${originalPath})`); + } + else if (this.entry.type !== FileType.Directory) { + throw ERR_UNKNOWN_INTERNAL('Internal error deleting from IndexedDBFSNode -- expected directory at ' + this.entry.path); + } + else if (pathParts.length === 1) { + this.entry.children.delete(pathParts[0]); + } + else { + const next = this.entry.children.get(pathParts[0]); + if (!next) { + throw ERR_UNKNOWN_INTERNAL('Internal error deleting from IndexedDBFSNode -- expected entry at ' + this.entry.path + '/' + next); + } + next.doDelete(pathParts.slice(1), originalPath); + } + }; + + add(path: string, entry: { type: 'file', size?: number } | { type: 'dir' }) { + this.doAdd(path.split('/').filter(p => p.length), entry, path); + } + + private doAdd(pathParts: string[], entry: { type: 'file', size?: number } | { type: 'dir' }, originalPath: string) { + if (pathParts.length === 0) { + throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- adding empty path (encountered while adding ${originalPath})`); + } + else if (this.entry.type !== FileType.Directory) { + throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- parent is not a directory (encountered while adding ${originalPath})`); + } + else if (pathParts.length === 1) { + const next = pathParts[0]; + const existing = this.entry.children.get(next); + if (entry.type === 'dir') { + if (existing?.entry.type === FileType.File) { + throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- overwriting file with directory: ${this.entry.path}/${next} (encountered while adding ${originalPath})`); + } + this.entry.children.set(next, existing ?? new IndexedDBFileSystemNode({ + type: FileType.Directory, + path: this.entry.path + '/' + next, + children: new Map(), + })); + } else { + if (existing?.entry.type === FileType.Directory) { + throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- overwriting directory with file: ${this.entry.path}/${next} (encountered while adding ${originalPath})`); + } + this.entry.children.set(next, new IndexedDBFileSystemNode({ + type: FileType.File, + path: this.entry.path + '/' + next, + size: entry.size, + })); + } + } + else if (pathParts.length > 1) { + const next = pathParts[0]; + let childNode = this.entry.children.get(next); + if (!childNode) { + childNode = new IndexedDBFileSystemNode({ + children: new Map(), + path: this.entry.path + '/' + next, + type: FileType.Directory + }); + this.entry.children.set(next, childNode); + } + else if (childNode.type === FileType.File) { + throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- overwriting file entry with directory: ${this.entry.path}/${next} (encountered while adding ${originalPath})`); + } + childNode.doAdd(pathParts.slice(1), entry, originalPath); + } + } + + print(indentation = '') { + console.log(indentation + this.entry.path); + if (this.entry.type === FileType.Directory) { + this.entry.children.forEach(child => child.print(indentation + ' ')); + } + } +} + class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSystemProvider { readonly capabilities: FileSystemProviderCapabilities = @@ -83,11 +220,14 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy readonly onDidChangeFile: Event = this._onDidChangeFile.event; private readonly versions: Map = new Map(); - private readonly dirs: Set = new Set(); - constructor(private readonly scheme: string, private readonly database: IDBDatabase, private readonly store: string) { + private cachedFiletree: Promise | undefined; + private writeManyThrottler: Throttler; + + constructor(scheme: string, private readonly database: IDBDatabase, private readonly store: string) { super(); - this.dirs.add('/'); + this.writeManyThrottler = new Throttler(); + } watch(resource: URI, opts: IWatchOptions): IDisposable { @@ -98,29 +238,22 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy try { const resourceStat = await this.stat(resource); if (resourceStat.type === FileType.File) { - throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory); + throw ERR_FILE_NOT_DIR; } } catch (error) { /* Ignore */ } - - // Make sure parent dir exists - await this.stat(dirname(resource)); - - this.dirs.add(resource.path); + (await this.getFiletree()).add(resource.path, { type: 'dir' }); } async stat(resource: URI): Promise { - try { - const content = await this.readFile(resource); + const content = (await this.getFiletree()).read(resource.path); + if (content?.type === FileType.File) { return { type: FileType.File, ctime: 0, mtime: this.versions.get(resource.toString()) || 0, - size: content.byteLength + size: content.size ?? (await this.readFile(resource)).byteLength }; - } catch (e) { - } - const files = await this.readdir(resource); - if (files.length) { + } else if (content?.type === FileType.Directory) { return { type: FileType.Directory, ctime: 0, @@ -128,75 +261,112 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy size: 0 }; } - if (this.dirs.has(resource.path)) { - return { - type: FileType.Directory, - ctime: 0, - mtime: 0, - size: 0 - }; + else { + throw ERR_FILE_NOT_FOUND; } - throw createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound); } - async readdir(resource: URI): Promise<[string, FileType][]> { - const hasKey = await this.hasKey(resource.path); - if (hasKey) { - throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory); + async readdir(resource: URI): Promise { + const entry = (await this.getFiletree()).read(resource.path); + if (!entry) { + // Dirs aren't saved to disk, so empty dirs will be lost on reload. + // Thus we have two options for what happens when you try to read a dir and nothing is found: + // - Throw FileSystemProviderErrorCode.FileNotFound + // - Return [] + // We choose to return [] as creating a dir then reading it (even after reload) should not throw an error. + return []; } - const keys = await this.getAllKeys(); - const files: Map = new Map(); - for (const key of keys) { - const keyResource = this.toResource(key); - if (extUri.isEqualOrParent(keyResource, resource)) { - const path = extUri.relativePath(resource, keyResource); - if (path) { - const keySegments = path.split('/'); - files.set(keySegments[0], [keySegments[0], keySegments.length === 1 ? FileType.File : FileType.Directory]); - } - } + if (entry.type !== FileType.Directory) { + throw ERR_FILE_NOT_DIR; + } + else { + return [...entry.children.entries()].map(([name, node]) => [name, node.type]); } - return [...files.values()]; } async readFile(resource: URI): Promise { - const hasKey = await this.hasKey(resource.path); - if (!hasKey) { - throw createFileSystemProviderError(localize('fileNotFound', "File not found"), FileSystemProviderErrorCode.FileNotFound); - } - const value = await this.getValue(resource.path); - if (typeof value === 'string') { - return VSBuffer.fromString(value).buffer; - } else { - return value; - } + const buffer = await new Promise((c, e) => { + const transaction = this.database.transaction([this.store]); + const objectStore = transaction.objectStore(this.store); + const request = objectStore.get(resource.path); + request.onerror = () => e(request.error); + request.onsuccess = () => { + if (request.result instanceof Uint8Array) { + c(request.result); + } else if (typeof request.result === 'string') { + c(VSBuffer.fromString(request.result).buffer); + } + else { + if (request.result === undefined) { + e(ERR_FILE_NOT_FOUND); + } else { + e(ERR_UNKNOWN_INTERNAL(`IndexedDB entry at "${resource.path}" in unexpected format`)); + } + } + }; + }); + + (await this.getFiletree()).add(resource.path, { type: 'file', size: buffer.byteLength }); + return buffer; } async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { - const hasKey = await this.hasKey(resource.path); - if (!hasKey) { - const files = await this.readdir(resource); - if (files.length) { - throw createFileSystemProviderError(localize('fileIsDirectory', "File is Directory"), FileSystemProviderErrorCode.FileIsADirectory); - } + const existing = await this.stat(resource).catch(() => undefined); + if (existing?.type === FileType.Directory) { + throw ERR_FILE_IS_DIR; } - await this.setValue(resource.path, content); + + this.fileWriteBatch.push({ content, resource }); + await this.writeManyThrottler.queue(() => this.writeMany()); + (await this.getFiletree()).add(resource.path, { type: 'file', size: content.byteLength }); this.versions.set(resource.toString(), (this.versions.get(resource.toString()) || 0) + 1); this._onDidChangeFile.fire([{ resource, type: FileChangeType.UPDATED }]); } async delete(resource: URI, opts: FileDeleteOptions): Promise { - const hasKey = await this.hasKey(resource.path); - if (hasKey) { - await this.deleteKey(resource.path); - this.versions.delete(resource.path); - this._onDidChangeFile.fire([{ resource, type: FileChangeType.DELETED }]); - return; + let stat: IStat; + try { + stat = await this.stat(resource); + } catch (e) { + if (e.code === FileSystemProviderErrorCode.FileNotFound) { + return; + } + throw e; } + let toDelete: string[]; if (opts.recursive) { - const files = await this.readdir(resource); - await Promise.all(files.map(([key]) => this.delete(joinPath(resource, key), opts))); + const tree = (await this.tree(resource)); + toDelete = tree.map(([path]) => path); + } else { + if (stat.type === FileType.Directory && (await this.readdir(resource)).length) { + throw ERR_DIR_NOT_EMPTY; + } + toDelete = [resource.path]; + } + await this.deleteKeys(toDelete); + (await this.getFiletree()).delete(resource.path); + toDelete.forEach(key => this.versions.delete(key)); + this._onDidChangeFile.fire(toDelete.map(path => ({ resource: resource.with({ path }), type: FileChangeType.DELETED }))); + } + + private async tree(resource: URI): Promise { + if ((await this.stat(resource)).type === FileType.Directory) { + const topLevelEntries = (await this.readdir(resource)).map(([key, type]) => { + return [joinPath(resource, key).path, type] as [string, FileType]; + }); + let allEntries = topLevelEntries; + await Promise.all(topLevelEntries.map( + async ([key, type]) => { + if (type === FileType.Directory) { + const childEntries = (await this.tree(resource.with({ path: key }))); + allEntries = allEntries.concat(childEntries); + } + })); + return allEntries; + } else { + const entries: DirEntry[] = [[resource.path, FileType.File]]; + return entries; } } @@ -204,58 +374,57 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy return Promise.reject(new Error('Not Supported')); } - private toResource(key: string): URI { - return URI.file(key).with({ scheme: this.scheme }); + private getFiletree(): Promise { + if (!this.cachedFiletree) { + this.cachedFiletree = new Promise((c, e) => { + const transaction = this.database.transaction([this.store]); + const objectStore = transaction.objectStore(this.store); + const request = objectStore.getAllKeys(); + request.onerror = () => e(request.error); + request.onsuccess = () => { + const rootNode = new IndexedDBFileSystemNode({ + children: new Map(), + path: '', + type: FileType.Directory + }); + const keys = request.result.map(key => key.toString()); + keys.forEach(key => rootNode.add(key, { type: 'file' })); + c(rootNode); + }; + }); + } + return this.cachedFiletree; } - async getAllKeys(): Promise { - return new Promise(async (c, e) => { - const transaction = this.database.transaction([this.store]); - const objectStore = transaction.objectStore(this.store); - const request = objectStore.getAllKeys(); - request.onerror = () => e(request.error); - request.onsuccess = () => c(request.result); - }); - } + private fileWriteBatch: { resource: URI, content: Uint8Array }[] = []; + private async writeMany() { + return new Promise((c, e) => { + const fileBatch = this.fileWriteBatch; + this.fileWriteBatch = []; + if (fileBatch.length === 0) { return c(); } - hasKey(key: string): Promise { - return new Promise(async (c, e) => { - const transaction = this.database.transaction([this.store]); - const objectStore = transaction.objectStore(this.store); - const request = objectStore.getKey(key); - request.onerror = () => e(request.error); - request.onsuccess = () => { - c(!!request.result); - }; - }); - } - - getValue(key: string): Promise { - return new Promise(async (c, e) => { - const transaction = this.database.transaction([this.store]); - const objectStore = transaction.objectStore(this.store); - const request = objectStore.get(key); - request.onerror = () => e(request.error); - request.onsuccess = () => c(request.result || ''); - }); - } - - setValue(key: string, value: Uint8Array): Promise { - return new Promise(async (c, e) => { const transaction = this.database.transaction([this.store], 'readwrite'); + transaction.onerror = () => e(transaction.error); const objectStore = transaction.objectStore(this.store); - const request = objectStore.put(value, key); - request.onerror = () => e(request.error); + let request: IDBRequest = undefined!; + for (const entry of fileBatch) { + request = objectStore.put(entry.content, entry.resource.path); + } request.onsuccess = () => c(); }); } - deleteKey(key: string): Promise { + private deleteKeys(keys: string[]): Promise { return new Promise(async (c, e) => { + if (keys.length === 0) { return c(); } const transaction = this.database.transaction([this.store], 'readwrite'); + transaction.onerror = () => e(transaction.error); const objectStore = transaction.objectStore(this.store); - const request = objectStore.delete(key); - request.onerror = () => e(request.error); + let request: IDBRequest = undefined!; + for (const key of keys) { + request = objectStore.delete(key); + } + request.onsuccess = () => c(); }); } diff --git a/src/vs/platform/files/common/fileService.ts b/src/vs/platform/files/common/fileService.ts index 23001b2a..dbc98c13 100644 --- a/src/vs/platform/files/common/fileService.ts +++ b/src/vs/platform/files/common/fileService.ts @@ -3,18 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; +import { mark } from 'vs/base/common/performance'; import { Disposable, IDisposable, toDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { IFileService, IResolveFileOptions, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, IWriteFileOptions, IReadFileOptions, IFileStreamContent, IFileContent, ETAG_DISABLED, hasFileReadStreamCapability, IFileSystemProviderWithFileReadStreamCapability, ensureFileSystemProviderError, IFileSystemProviderCapabilitiesChangeEvent } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; -import { isAbsolutePath, dirname, basename, joinPath, IExtUri, extUri, extUriIgnorePathCase } from 'vs/base/common/resources'; -import { localize } from 'vs/nls'; +import { IExtUri, extUri, extUriIgnorePathCase, isAbsolutePath } from 'vs/base/common/resources'; import { TernarySearchTree } from 'vs/base/common/map'; import { isNonEmptyArray, coalesce } from 'vs/base/common/arrays'; -import { getBaseLabel } from 'vs/base/common/labels'; import { ILogService } from 'vs/platform/log/common/log'; -import { VSBuffer, VSBufferReadable, readableToBuffer, bufferToReadable, streamToBuffer, bufferToStream, VSBufferReadableStream, VSBufferReadableBufferedStream, bufferedStreamToBuffer, newWriteableBufferStream } from 'vs/base/common/buffer'; -import { isReadableStream, transform, peekReadable, peekStream, isReadableBufferedStream } from 'vs/base/common/stream'; +import { VSBuffer, VSBufferReadable, readableToBuffer, bufferToReadable, streamToBuffer, VSBufferReadableStream, VSBufferReadableBufferedStream, bufferedStreamToBuffer, newWriteableBufferStream } from 'vs/base/common/buffer'; +import { isReadableStream, transform, peekReadable, peekStream, isReadableBufferedStream, newWriteableStream, IReadableStreamObservable, observe } from 'vs/base/common/stream'; import { Queue } from 'vs/base/common/async'; import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; import { Schemas } from 'vs/base/common/network'; @@ -49,6 +49,8 @@ export class FileService extends Disposable implements IFileService { throw new Error(`A filesystem provider for the scheme '${scheme}' is already registered.`); } + mark(`code/registerFilesystem/${scheme}`); + // Add provider with event this.provider.set(scheme, provider); this._onDidChangeFileSystemProviderRegistrations.fire({ added: true, scheme, provider }); @@ -102,7 +104,7 @@ export class FileService extends Disposable implements IFileService { return !!(provider && (provider.capabilities & capability)); } - listCapabilities(): Iterable<{ scheme: string, capabilities: FileSystemProviderCapabilities }> { + listCapabilities(): Iterable<{ scheme: string, capabilities: FileSystemProviderCapabilities; }> { return Iterable.map(this.provider, ([scheme, provider]) => ({ scheme, capabilities: provider.capabilities })); } @@ -215,14 +217,15 @@ export class FileService extends Disposable implements IFileService { }); } - private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType } & Partial, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise; + private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType; } & Partial, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise; private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat, siblings: number | undefined, resolveMetadata: true, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise; - private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType } & Partial, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise { + private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType; } & Partial, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise { + const { providerExtUri } = this.getExtUri(provider); // convert to file stat const fileStat: IFileStat = { resource, - name: getBaseLabel(resource), + name: providerExtUri.basename(resource), isFile: (stat.type & FileType.File) !== 0, isDirectory: (stat.type & FileType.Directory) !== 0, isSymbolicLink: (stat.type & FileType.SymbolicLink) !== 0, @@ -238,7 +241,7 @@ export class FileService extends Disposable implements IFileService { const entries = await provider.readdir(resource); const resolvedEntries = await Promise.all(entries.map(async ([name, type]) => { try { - const childResource = joinPath(resource, name); + const childResource = providerExtUri.joinPath(resource, name); const childStat = resolveMetadata ? await provider.stat(childResource) : { type }; return await this.toFileStat(provider, childResource, childStat, entries.length, resolveMetadata, recurse); @@ -263,8 +266,8 @@ export class FileService extends Disposable implements IFileService { return fileStat; } - async resolveAll(toResolve: { resource: URI, options?: IResolveFileOptions }[]): Promise; - async resolveAll(toResolve: { resource: URI, options: IResolveMetadataFileOptions }[]): Promise; + async resolveAll(toResolve: { resource: URI, options?: IResolveFileOptions; }[]): Promise; + async resolveAll(toResolve: { resource: URI, options: IResolveMetadataFileOptions; }[]): Promise; async resolveAll(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): Promise { return Promise.all(toResolve.map(async entry => { try { @@ -327,6 +330,7 @@ export class FileService extends Disposable implements IFileService { async writeFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise { const provider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(resource), resource); + const { providerExtUri } = this.getExtUri(provider); try { @@ -335,7 +339,7 @@ export class FileService extends Disposable implements IFileService { // mkdir recursively as needed if (!stat) { - await this.mkdirp(provider, dirname(resource)); + await this.mkdirp(provider, providerExtUri.dirname(resource)); } // optimization: if the provider has unbuffered write capability and the data @@ -435,7 +439,7 @@ export class FileService extends Disposable implements IFileService { return this.doReadAsFileStream(provider, resource, options); } - private async doReadAsFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions & { preferUnbuffered?: boolean }): Promise { + private async doReadAsFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions & { preferUnbuffered?: boolean; }): Promise { // install a cancellation token that gets cancelled // when any error occurs. this allows us to resolve @@ -450,6 +454,8 @@ export class FileService extends Disposable implements IFileService { throw error; }); + let fileStreamObserver: IReadableStreamObservable | undefined = undefined; + try { // if the etag is provided, we await the result of the validation @@ -460,30 +466,41 @@ export class FileService extends Disposable implements IFileService { await statPromise; } - let fileStreamPromise: Promise; + let fileStream: VSBufferReadableStream | undefined = undefined; // read unbuffered (only if either preferred, or the provider has no buffered read capability) if (!(hasOpenReadWriteCloseCapability(provider) || hasFileReadStreamCapability(provider)) || (hasReadWriteCapability(provider) && options?.preferUnbuffered)) { - fileStreamPromise = this.readFileUnbuffered(provider, resource, options); + fileStream = this.readFileUnbuffered(provider, resource, options); } // read streamed (always prefer over primitive buffered read) else if (hasFileReadStreamCapability(provider)) { - fileStreamPromise = Promise.resolve(this.readFileStreamed(provider, resource, cancellableSource.token, options)); + fileStream = this.readFileStreamed(provider, resource, cancellableSource.token, options); } // read buffered else { - fileStreamPromise = Promise.resolve(this.readFileBuffered(provider, resource, cancellableSource.token, options)); + fileStream = this.readFileBuffered(provider, resource, cancellableSource.token, options); } - const [fileStat, fileStream] = await Promise.all([statPromise, fileStreamPromise]); + // observe the stream for the error case below + fileStreamObserver = observe(fileStream); + + const fileStat = await statPromise; return { ...fileStat, value: fileStream }; } catch (error) { + + // Await the stream to finish so that we exit this method + // in a consistent state with file handles closed + // (https://github.com/microsoft/vscode/issues/114024) + if (fileStreamObserver) { + await fileStreamObserver.errorOrEnd(); + } + throw new FileOperationError(localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options); } } @@ -509,23 +526,36 @@ export class FileService extends Disposable implements IFileService { return stream; } - private async readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options?: IReadFileOptions): Promise { - let buffer = await provider.readFile(resource); + private readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options?: IReadFileOptions): VSBufferReadableStream { + const stream = newWriteableStream(data => VSBuffer.concat(data)); - // respect position option - if (options && typeof options.position === 'number') { - buffer = buffer.slice(options.position); - } + // Read the file into the stream async but do not wait for + // this to complete because streams work via events + (async () => { + try { + let buffer = await provider.readFile(resource); - // respect length option - if (options && typeof options.length === 'number') { - buffer = buffer.slice(0, options.length); - } + // respect position option + if (options && typeof options.position === 'number') { + buffer = buffer.slice(options.position); + } - // Throw if file is too large to load - this.validateReadFileLimits(resource, buffer.byteLength, options); + // respect length option + if (options && typeof options.length === 'number') { + buffer = buffer.slice(0, options.length); + } - return bufferToStream(VSBuffer.wrap(buffer)); + // Throw if file is too large to load + this.validateReadFileLimits(resource, buffer.byteLength, options); + + // End stream with data + stream.end(VSBuffer.wrap(buffer)); + } catch (err) { + stream.error(err); + } + })(); + + return stream; } private async validateReadFile(resource: URI, options?: IReadFileOptions): Promise { @@ -634,7 +664,7 @@ export class FileService extends Disposable implements IFileService { } // create parent folders - await this.mkdirp(targetProvider, dirname(target)); + await this.mkdirp(targetProvider, this.getExtUri(targetProvider).providerExtUri.dirname(target)); // copy source => target if (mode === 'copy') { @@ -671,7 +701,6 @@ export class FileService extends Disposable implements IFileService { // across providers: copy to target & delete at source else { await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', overwrite); - await this.del(source, { recursive: true }); return 'copy'; @@ -710,7 +739,7 @@ export class FileService extends Disposable implements IFileService { // create children in target if (Array.isArray(sourceFolder.children)) { await Promise.all(sourceFolder.children.map(async sourceChild => { - const targetChild = joinPath(targetFolder, sourceChild.name); + const targetChild = this.getExtUri(targetProvider).providerExtUri.joinPath(targetFolder, sourceChild.name); if (sourceChild.isDirectory) { return this.doCopyFolder(sourceProvider, await this.resolve(sourceChild.resource), targetProvider, targetChild); } else { @@ -720,21 +749,21 @@ export class FileService extends Disposable implements IFileService { } } - private async doValidateMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<{ exists: boolean, isSameResourceWithDifferentPathCase: boolean }> { + private async doValidateMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<{ exists: boolean, isSameResourceWithDifferentPathCase: boolean; }> { let isSameResourceWithDifferentPathCase = false; // Check if source is equal or parent to target (requires providers to be the same) if (sourceProvider === targetProvider) { - const { extUri, isPathCaseSensitive } = this.getExtUri(sourceProvider); + const { providerExtUri, isPathCaseSensitive } = this.getExtUri(sourceProvider); if (!isPathCaseSensitive) { - isSameResourceWithDifferentPathCase = extUri.isEqual(source, target); + isSameResourceWithDifferentPathCase = providerExtUri.isEqual(source, target); } if (isSameResourceWithDifferentPathCase && mode === 'copy') { throw new Error(localize('unableToMoveCopyError1', "Unable to copy when source '{0}' is same as target '{1}' with different path case on a case insensitive file system", this.resourceForError(source), this.resourceForError(target))); } - if (!isSameResourceWithDifferentPathCase && extUri.isEqualOrParent(target, source)) { + if (!isSameResourceWithDifferentPathCase && providerExtUri.isEqualOrParent(target, source)) { throw new Error(localize('unableToMoveCopyError2', "Unable to move/copy when source '{0}' is parent of target '{1}'.", this.resourceForError(source), this.resourceForError(target))); } } @@ -751,8 +780,8 @@ export class FileService extends Disposable implements IFileService { // Special case: if the target is a parent of the source, we cannot delete // it as it would delete the source as well. In this case we have to throw if (sourceProvider === targetProvider) { - const { extUri } = this.getExtUri(sourceProvider); - if (extUri.isEqualOrParent(source, target)) { + const { providerExtUri } = this.getExtUri(sourceProvider); + if (providerExtUri.isEqualOrParent(source, target)) { 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))); } } @@ -761,11 +790,11 @@ export class FileService extends Disposable implements IFileService { return { exists, isSameResourceWithDifferentPathCase }; } - private getExtUri(provider: IFileSystemProvider): { extUri: IExtUri, isPathCaseSensitive: boolean } { + private getExtUri(provider: IFileSystemProvider): { providerExtUri: IExtUri, isPathCaseSensitive: boolean; } { const isPathCaseSensitive = this.isPathCaseSensitive(provider); return { - extUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase, + providerExtUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase, isPathCaseSensitive }; } @@ -791,8 +820,8 @@ export class FileService extends Disposable implements IFileService { const directoriesToCreate: string[] = []; // mkdir until we reach root - const { extUri } = this.getExtUri(provider); - while (!extUri.isEqual(directory, dirname(directory))) { + const { providerExtUri } = this.getExtUri(provider); + while (!providerExtUri.isEqual(directory, providerExtUri.dirname(directory))) { try { const stat = await provider.stat(directory); if ((stat.type & FileType.Directory) === 0) { @@ -808,16 +837,16 @@ export class FileService extends Disposable implements IFileService { } // Upon error, remember directories that need to be created - directoriesToCreate.push(basename(directory)); + directoriesToCreate.push(providerExtUri.basename(directory)); // Continue up - directory = dirname(directory); + directory = providerExtUri.dirname(directory); } } // Create directories as needed for (let i = directoriesToCreate.length - 1; i >= 0; i--) { - directory = joinPath(directory, directoriesToCreate[i]); + directory = providerExtUri.joinPath(directory, directoriesToCreate[i]); try { await provider.mkdir(directory); @@ -894,11 +923,11 @@ export class FileService extends Disposable implements IFileService { private readonly _onDidFilesChange = this._register(new Emitter()); readonly onDidFilesChange = this._onDidFilesChange.event; - private readonly activeWatchers = new Map(); + private readonly activeWatchers = new Map(); watch(resource: URI, options: IWatchOptions = { recursive: false, excludes: [] }): IDisposable { let watchDisposed = false; - let watchDisposable = toDisposable(() => watchDisposed = true); + let disposeWatch = () => { watchDisposed = true; }; // Watch and wire in disposable which is async but // check if we got disposed meanwhile and forward @@ -906,11 +935,11 @@ export class FileService extends Disposable implements IFileService { if (watchDisposed) { dispose(disposable); } else { - watchDisposable = disposable; + disposeWatch = () => dispose(disposable); } }, error => this.logService.error(error)); - return toDisposable(() => dispose(watchDisposable)); + return toDisposable(() => disposeWatch()); } async doWatch(resource: URI, options: IWatchOptions): Promise { @@ -940,12 +969,12 @@ export class FileService extends Disposable implements IFileService { } private toWatchKey(provider: IFileSystemProvider, resource: URI, options: IWatchOptions): string { - const { extUri } = this.getExtUri(provider); + const { providerExtUri } = this.getExtUri(provider); return [ - extUri.getComparisonKey(resource), // lowercase path if the provider is case insensitive - String(options.recursive), // use recursive: true | false as part of the key - options.excludes.join() // use excludes as part of the key + providerExtUri.getComparisonKey(resource), // lowercase path if the provider is case insensitive + String(options.recursive), // use recursive: true | false as part of the key + options.excludes.join() // use excludes as part of the key ].join(); } @@ -963,8 +992,8 @@ export class FileService extends Disposable implements IFileService { private readonly writeQueues: Map> = new Map(); private ensureWriteQueue(provider: IFileSystemProvider, resource: URI): Queue { - const { extUri } = this.getExtUri(provider); - const queueKey = extUri.getComparisonKey(resource); + const { providerExtUri } = this.getExtUri(provider); + const queueKey = providerExtUri.getComparisonKey(resource); // ensure to never write to the same resource without finishing // the one write. this ensures a write finishes consistently diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 4ae15434..9b91c3dc 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -278,7 +278,7 @@ export interface IFileSystemProvider { readonly capabilities: FileSystemProviderCapabilities; readonly onDidChangeCapabilities: Event; - readonly onDidErrorOccur?: Event; // TODO@ben remove once file watchers are solid + readonly onDidErrorOccur?: Event; // TODO@bpasero remove once file watchers are solid readonly onDidChangeFile: Event; watch(resource: URI, opts: IWatchOptions): IDisposable; @@ -947,9 +947,9 @@ export function etag(stat: { mtime: number | undefined, size: number | undefined return stat.mtime.toString(29) + stat.size.toString(31); } -export function whenProviderRegistered(file: URI, fileService: IFileService): Promise { +export async function whenProviderRegistered(file: URI, fileService: IFileService): Promise { if (fileService.canHandleResource(URI.from({ scheme: file.scheme }))) { - return Promise.resolve(); + return; } return new Promise(resolve => { diff --git a/src/vs/platform/files/common/io.ts b/src/vs/platform/files/common/io.ts index 14124115..592b5116 100644 --- a/src/vs/platform/files/common/io.ts +++ b/src/vs/platform/files/common/io.ts @@ -58,10 +58,11 @@ async function doReadFileIntoStream(provider: IFileSystemProviderWithOpenRead // open handle through provider const handle = await provider.open(resource, { create: false }); - // Check for cancellation - throwIfCancelled(token); - try { + + // Check for cancellation + throwIfCancelled(token); + let totalBytesRead = 0; let bytesRead = 0; let allowedRemainingBytes = (options && typeof options.length === 'number') ? options.length : undefined; diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index 89408a3f..75811f56 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -522,7 +522,7 @@ export class DiskFileSystemProvider extends Disposable implements return this.watchRecursive(resource, opts.excludes); } - return this.watchNonRecursive(resource); // TODO@ben ideally the same watcher can be used in both cases + return this.watchNonRecursive(resource); // TODO@bpasero ideally the same watcher can be used in both cases } private watchRecursive(resource: URI, excludes: string[]): IDisposable { diff --git a/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts b/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts index 51a4b1a6..70c4cfd6 100644 --- a/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts +++ b/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as glob from 'vs/base/common/glob'; -import * as extpath from 'vs/base/common/extpath'; -import * as path from 'vs/base/common/path'; -import * as platform from 'vs/base/common/platform'; -import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; import * as nsfw from 'vscode-nsfw'; +import * as glob from 'vs/base/common/glob'; +import { join } from 'vs/base/common/path'; +import { isMacintosh } from 'vs/base/common/platform'; +import { isEqualOrParent } from 'vs/base/common/extpath'; +import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; import { IWatcherService, IWatcherRequest } from 'vs/platform/files/node/watcher/nsfw/watcher'; import { ThrottledDelayer } from 'vs/base/common/async'; import { FileChangeType } from 'vs/platform/files/common/files'; @@ -111,7 +111,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { // We have to detect this case and massage the events to correct this. let realBasePathDiffers = false; let realBasePathLength = request.path.length; - if (platform.isMacintosh) { + if (isMacintosh) { try { // First check for symbolic link @@ -141,7 +141,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { for (const e of events) { // Logging if (this.verboseLogging) { - const logPath = e.action === nsfw.actions.RENAMED ? path.join(e.directory, e.oldFile || '') + ' -> ' + e.newFile : path.join(e.directory, e.file || ''); + const logPath = e.action === nsfw.actions.RENAMED ? join(e.directory, e.oldFile || '') + ' -> ' + e.newFile : join(e.directory, e.file || ''); this.log(`${e.action === nsfw.actions.CREATED ? '[CREATED]' : e.action === nsfw.actions.DELETED ? '[DELETED]' : e.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]'} ${logPath}`); } @@ -149,20 +149,20 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { let absolutePath: string; if (e.action === nsfw.actions.RENAMED) { // Rename fires when a file's name changes within a single directory - absolutePath = path.join(e.directory, e.oldFile || ''); + absolutePath = join(e.directory, e.oldFile || ''); if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) { undeliveredFileEvents.push({ type: FileChangeType.DELETED, path: absolutePath }); } else if (this.verboseLogging) { this.log(` >> ignored ${absolutePath}`); } - absolutePath = path.join(e.newDirectory || e.directory, e.newFile || ''); + absolutePath = join(e.newDirectory || e.directory, e.newFile || ''); if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) { undeliveredFileEvents.push({ type: FileChangeType.ADDED, path: absolutePath }); } else if (this.verboseLogging) { this.log(` >> ignored ${absolutePath}`); } } else { - absolutePath = path.join(e.directory, e.file || ''); + absolutePath = join(e.directory, e.file || ''); if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) { undeliveredFileEvents.push({ type: nsfwActionToRawChangeType[e.action], @@ -179,7 +179,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { const events = undeliveredFileEvents; undeliveredFileEvents = []; - if (platform.isMacintosh) { + if (isMacintosh) { events.forEach(e => { // Mac uses NFD unicode form on disk, but we want NFC @@ -230,7 +230,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { // Normalizes a set of root paths by removing any root paths that are // sub-paths of other roots. return roots.filter(r => roots.every(other => { - return !(r.path.length > other.path.length && extpath.isEqualOrParent(r.path, other.path)); + return !(r.path.length > other.path.length && isEqualOrParent(r.path, other.path)); })); } diff --git a/src/vs/platform/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts b/src/vs/platform/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts index 9ad083fc..0469c3f6 100644 --- a/src/vs/platform/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts +++ b/src/vs/platform/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts @@ -30,28 +30,28 @@ suite('NSFW Watcher Service', async () => { test('should not impacts roots that don\'t overlap', () => { const service = new TestNsfwWatcherService(); if (platform.isWindows) { - assert.deepEqual(service.normalizeRoots(['C:\\a']), ['C:\\a']); - assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']); - assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']); + assert.deepStrictEqual(service.normalizeRoots(['C:\\a']), ['C:\\a']); + assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']); + assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']); } else { - assert.deepEqual(service.normalizeRoots(['/a']), ['/a']); - assert.deepEqual(service.normalizeRoots(['/a', '/b']), ['/a', '/b']); - assert.deepEqual(service.normalizeRoots(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']); + assert.deepStrictEqual(service.normalizeRoots(['/a']), ['/a']); + assert.deepStrictEqual(service.normalizeRoots(['/a', '/b']), ['/a', '/b']); + assert.deepStrictEqual(service.normalizeRoots(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']); } }); test('should remove sub-folders of other roots', () => { const service = new TestNsfwWatcherService(); if (platform.isWindows) { - assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b']), ['C:\\a']); - assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); - assert.deepEqual(service.normalizeRoots(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); - assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']); + assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b']), ['C:\\a']); + assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); + assert.deepStrictEqual(service.normalizeRoots(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); + assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']); } else { - assert.deepEqual(service.normalizeRoots(['/a', '/a/b']), ['/a']); - assert.deepEqual(service.normalizeRoots(['/a', '/b', '/a/b']), ['/a', '/b']); - assert.deepEqual(service.normalizeRoots(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']); - assert.deepEqual(service.normalizeRoots(['/a', '/a/b', '/a/c/d']), ['/a']); + assert.deepStrictEqual(service.normalizeRoots(['/a', '/a/b']), ['/a']); + assert.deepStrictEqual(service.normalizeRoots(['/a', '/b', '/a/b']), ['/a', '/b']); + assert.deepStrictEqual(service.normalizeRoots(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']); + assert.deepStrictEqual(service.normalizeRoots(['/a', '/a/b', '/a/c/d']), ['/a']); } }); }); diff --git a/src/vs/platform/files/node/watcher/nsfw/watcherService.ts b/src/vs/platform/files/node/watcher/nsfw/watcherService.ts index 79fa3ba2..3b0cd433 100644 --- a/src/vs/platform/files/node/watcher/nsfw/watcherService.ts +++ b/src/vs/platform/files/node/watcher/nsfw/watcherService.ts @@ -39,9 +39,9 @@ export class FileWatcher extends Disposable { serverName: 'File Watcher (nsfw)', args: ['--type=watcherService'], env: { - AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/nsfw/watcherApp', - PIPE_LOGGING: 'true', - VERBOSE_LOGGING: 'true' // transmit console logs from server to client + VSCODE_AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/nsfw/watcherApp', + VSCODE_PIPE_LOGGING: 'true', + VSCODE_VERBOSE_LOGGING: 'true' // transmit console logs from server to client } } )); diff --git a/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts b/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts index 895e5dfa..25276aa2 100644 --- a/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts +++ b/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts @@ -6,9 +6,8 @@ import * as chokidar from 'chokidar'; import * as fs from 'fs'; import * as gracefulFs from 'graceful-fs'; -gracefulFs.gracefulify(fs); -import * as extpath from 'vs/base/common/extpath'; import * as glob from 'vs/base/common/glob'; +import { isEqualOrParent } from 'vs/base/common/extpath'; import { FileChangeType } from 'vs/platform/files/common/files'; import { ThrottledDelayer } from 'vs/base/common/async'; import { normalizeNFC } from 'vs/base/common/normalization'; @@ -20,6 +19,8 @@ import { Emitter, Event } from 'vs/base/common/event'; import { equals } from 'vs/base/common/arrays'; import { Disposable } from 'vs/base/common/lifecycle'; +gracefulFs.gracefulify(fs); // enable gracefulFs + process.noAsar = true; // disable ASAR support in watcher process interface IWatcher { @@ -311,7 +312,7 @@ function isIgnored(path: string, requests: ExtendedWatcherRequest[]): boolean { return false; } - if (extpath.isEqualOrParent(path, request.path)) { + if (isEqualOrParent(path, request.path)) { if (!request.parsedPattern) { if (request.excludes && request.excludes.length > 0) { const pattern = `{${request.excludes.join(',')}}`; @@ -343,7 +344,7 @@ export function normalizeRoots(requests: IWatcherRequest[]): { [basePath: string for (const request of requests) { const basePath = request.path; const ignored = (request.excludes || []).sort(); - if (prevRequest && (extpath.isEqualOrParent(basePath, prevRequest.path))) { + if (prevRequest && (isEqualOrParent(basePath, prevRequest.path))) { if (!isEqualIgnore(ignored, prevRequest.excludes)) { result[prevRequest.path].push({ path: basePath, excludes: ignored }); } diff --git a/src/vs/platform/files/node/watcher/unix/test/chockidarWatcherService.test.ts b/src/vs/platform/files/node/watcher/unix/test/chockidarWatcherService.test.ts index 4c780ec6..8b475de9 100644 --- a/src/vs/platform/files/node/watcher/unix/test/chockidarWatcherService.test.ts +++ b/src/vs/platform/files/node/watcher/unix/test/chockidarWatcherService.test.ts @@ -20,18 +20,18 @@ suite('Chokidar normalizeRoots', async () => { function assertNormalizedRootPath(inputPaths: string[], expectedPaths: string[]) { const requests = inputPaths.map(path => newRequest(path)); const actual = normalizeRoots(requests); - assert.deepEqual(Object.keys(actual).sort(), expectedPaths); + assert.deepStrictEqual(Object.keys(actual).sort(), expectedPaths); } function assertNormalizedRequests(inputRequests: IWatcherRequest[], expectedRequests: { [path: string]: IWatcherRequest[] }) { const actual = normalizeRoots(inputRequests); const actualPath = Object.keys(actual).sort(); const expectedPaths = Object.keys(expectedRequests).sort(); - assert.deepEqual(actualPath, expectedPaths); + assert.deepStrictEqual(actualPath, expectedPaths); for (let path of actualPath) { let a = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path)); let e = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path)); - assert.deepEqual(a, e); + assert.deepStrictEqual(a, e); } } diff --git a/src/vs/platform/files/node/watcher/unix/watcherService.ts b/src/vs/platform/files/node/watcher/unix/watcherService.ts index e561cc8e..75f1bcf9 100644 --- a/src/vs/platform/files/node/watcher/unix/watcherService.ts +++ b/src/vs/platform/files/node/watcher/unix/watcherService.ts @@ -40,9 +40,9 @@ export class FileWatcher extends Disposable { serverName: 'File Watcher (chokidar)', args: ['--type=watcherService'], env: { - AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/unix/watcherApp', - PIPE_LOGGING: 'true', - VERBOSE_LOGGING: 'true' // transmit console logs from server to client + VSCODE_AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/unix/watcherApp', + VSCODE_PIPE_LOGGING: 'true', + VSCODE_VERBOSE_LOGGING: 'true' // transmit console logs from server to client } } )); diff --git a/src/vs/platform/files/test/browser/fileService.test.ts b/src/vs/platform/files/test/browser/fileService.test.ts index edc32f0a..e8d53c1c 100644 --- a/src/vs/platform/files/test/browser/fileService.test.ts +++ b/src/vs/platform/files/test/browser/fileService.test.ts @@ -19,7 +19,7 @@ suite('File Service', () => { const resource = URI.parse('test://foo/bar'); const provider = new NullFileSystemProvider(); - assert.equal(service.canHandleResource(resource), false); + assert.strictEqual(service.canHandleResource(resource), false); const registrations: IFileSystemProviderRegistrationEvent[] = []; service.onDidChangeFileSystemProviderRegistrations(e => { @@ -47,33 +47,33 @@ suite('File Service', () => { await service.activateProvider('test'); - assert.equal(service.canHandleResource(resource), true); + assert.strictEqual(service.canHandleResource(resource), true); - assert.equal(registrations.length, 1); - assert.equal(registrations[0].scheme, 'test'); - assert.equal(registrations[0].added, true); + assert.strictEqual(registrations.length, 1); + assert.strictEqual(registrations[0].scheme, 'test'); + assert.strictEqual(registrations[0].added, true); assert.ok(registrationDisposable); - assert.equal(capabilityChanges.length, 0); + assert.strictEqual(capabilityChanges.length, 0); provider.setCapabilities(FileSystemProviderCapabilities.FileFolderCopy); - assert.equal(capabilityChanges.length, 1); + assert.strictEqual(capabilityChanges.length, 1); provider.setCapabilities(FileSystemProviderCapabilities.Readonly); - assert.equal(capabilityChanges.length, 2); + assert.strictEqual(capabilityChanges.length, 2); await service.activateProvider('test'); - assert.equal(callCount, 2); // activation is called again + assert.strictEqual(callCount, 2); // activation is called again - assert.equal(service.hasCapability(resource, FileSystemProviderCapabilities.Readonly), true); - assert.equal(service.hasCapability(resource, FileSystemProviderCapabilities.FileOpenReadWriteClose), false); + assert.strictEqual(service.hasCapability(resource, FileSystemProviderCapabilities.Readonly), true); + assert.strictEqual(service.hasCapability(resource, FileSystemProviderCapabilities.FileOpenReadWriteClose), false); registrationDisposable!.dispose(); - assert.equal(service.canHandleResource(resource), false); + assert.strictEqual(service.canHandleResource(resource), false); - assert.equal(registrations.length, 2); - assert.equal(registrations[1].scheme, 'test'); - assert.equal(registrations[1].added, false); + assert.strictEqual(registrations.length, 2); + assert.strictEqual(registrations[1].scheme, 'test'); + assert.strictEqual(registrations[1].added, false); }); test('watch', async () => { @@ -91,9 +91,9 @@ suite('File Service', () => { const watcher1Disposable = service.watch(resource1); await timeout(0); // service.watch() is async - assert.equal(disposeCounter, 0); + assert.strictEqual(disposeCounter, 0); watcher1Disposable.dispose(); - assert.equal(disposeCounter, 1); + assert.strictEqual(disposeCounter, 1); disposeCounter = 0; const resource2 = URI.parse('test://foo/bar2'); @@ -102,13 +102,13 @@ suite('File Service', () => { const watcher2Disposable3 = service.watch(resource2); await timeout(0); // service.watch() is async - assert.equal(disposeCounter, 0); + assert.strictEqual(disposeCounter, 0); watcher2Disposable1.dispose(); - assert.equal(disposeCounter, 0); + assert.strictEqual(disposeCounter, 0); watcher2Disposable2.dispose(); - assert.equal(disposeCounter, 0); + assert.strictEqual(disposeCounter, 0); watcher2Disposable3.dispose(); - assert.equal(disposeCounter, 1); + assert.strictEqual(disposeCounter, 1); disposeCounter = 0; const resource3 = URI.parse('test://foo/bar3'); @@ -116,10 +116,10 @@ suite('File Service', () => { const watcher3Disposable2 = service.watch(resource3, { recursive: true, excludes: [] }); await timeout(0); // service.watch() is async - assert.equal(disposeCounter, 0); + assert.strictEqual(disposeCounter, 0); watcher3Disposable1.dispose(); - assert.equal(disposeCounter, 1); + assert.strictEqual(disposeCounter, 1); watcher3Disposable2.dispose(); - assert.equal(disposeCounter, 2); + assert.strictEqual(disposeCounter, 2); }); }); diff --git a/src/vs/platform/files/test/browser/indexedDBFileService.test.ts b/src/vs/platform/files/test/browser/indexedDBFileService.test.ts index abb3034a..67ccfd9a 100644 --- a/src/vs/platform/files/test/browser/indexedDBFileService.test.ts +++ b/src/vs/platform/files/test/browser/indexedDBFileService.test.ts @@ -6,20 +6,20 @@ import * as assert from 'assert'; import { FileService } from 'vs/platform/files/common/fileService'; import { Schemas } from 'vs/base/common/network'; -import { posix } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; -import { FileOperation, FileOperationEvent } from 'vs/platform/files/common/files'; +import { FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileSystemProviderErrorCode, FileType, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { NullLogService } from 'vs/platform/log/common/log'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IIndexedDBFileSystemProvider, IndexedDB, INDEXEDDB_LOGS_OBJECT_STORE, INDEXEDDB_USERDATA_OBJECT_STORE } from 'vs/platform/files/browser/indexedDBFileSystemProvider'; import { assertIsDefined } from 'vs/base/common/types'; - -// FileService doesn't work with \ leading a path. Windows join swaps /'s for \'s, -// making /-style absolute paths fail isAbsolute checks. -const join = posix.join; +import { basename, joinPath } from 'vs/base/common/resources'; +import { bufferToReadable, bufferToStream, VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; suite('IndexedDB File Service', function () { + // IDB sometimes under pressure in build machines. + this.retries(3); + const logSchema = 'logs'; let service: FileService; @@ -27,12 +27,43 @@ suite('IndexedDB File Service', function () { let userdataFileProvider: IIndexedDBFileSystemProvider; const testDir = '/'; - const makeLogfileURI = (path: string) => URI.from({ scheme: logSchema, path }); - const makeUserdataURI = (path: string) => URI.from({ scheme: Schemas.userData, path }); + const logfileURIFromPaths = (paths: string[]) => joinPath(URI.from({ scheme: logSchema, path: testDir }), ...paths); + const userdataURIFromPaths = (paths: readonly string[]) => joinPath(URI.from({ scheme: Schemas.userData, path: testDir }), ...paths); const disposables = new DisposableStore(); - setup(async () => { + const initFixtures = async () => { + await Promise.all( + [['fixtures', 'resolver', 'examples'], + ['fixtures', 'resolver', 'other', 'deep'], + ['fixtures', 'service', 'deep'], + ['batched']] + .map(path => userdataURIFromPaths(path)) + .map(uri => service.createFolder(uri))); + await Promise.all( + ([ + [['fixtures', 'resolver', 'examples', 'company.js'], 'class company {}'], + [['fixtures', 'resolver', 'examples', 'conway.js'], 'export function conway() {}'], + [['fixtures', 'resolver', 'examples', 'employee.js'], 'export const employee = "jax"'], + [['fixtures', 'resolver', 'examples', 'small.js'], ''], + [['fixtures', 'resolver', 'other', 'deep', 'company.js'], 'class company {}'], + [['fixtures', 'resolver', 'other', 'deep', 'conway.js'], 'export function conway() {}'], + [['fixtures', 'resolver', 'other', 'deep', 'employee.js'], 'export const employee = "jax"'], + [['fixtures', 'resolver', 'other', 'deep', 'small.js'], ''], + [['fixtures', 'resolver', 'index.html'], '

    p

    '], + [['fixtures', 'resolver', 'site.css'], '.p {color: red;}'], + [['fixtures', 'service', 'deep', 'company.js'], 'class company {}'], + [['fixtures', 'service', 'deep', 'conway.js'], 'export function conway() {}'], + [['fixtures', 'service', 'deep', 'employee.js'], 'export const employee = "jax"'], + [['fixtures', 'service', 'deep', 'small.js'], ''], + [['fixtures', 'service', 'binary.txt'], '

    p

    '], + ] as const) + .map(([path, contents]) => [userdataURIFromPaths(path), contents] as const) + .map(([uri, contents]) => service.createFile(uri, VSBuffer.fromString(contents))) + ); + }; + + const reload = async () => { const logService = new NullLogService(); service = new FileService(logService); @@ -45,33 +76,302 @@ suite('IndexedDB File Service', function () { userdataFileProvider = assertIsDefined(await new IndexedDB().createFileSystemProvider(logSchema, INDEXEDDB_USERDATA_OBJECT_STORE)); disposables.add(service.registerProvider(Schemas.userData, userdataFileProvider)); disposables.add(userdataFileProvider); + }; + + setup(async () => { + await reload(); }); teardown(async () => { disposables.clear(); + await logFileProvider.delete(logfileURIFromPaths([]), { recursive: true, useTrash: false }); + await userdataFileProvider.delete(userdataURIFromPaths([]), { recursive: true, useTrash: false }); + }); - await logFileProvider.delete(makeLogfileURI(testDir), { recursive: true, useTrash: false }); - await userdataFileProvider.delete(makeUserdataURI(testDir), { recursive: true, useTrash: false }); + test('root is always present', async () => { + assert.strictEqual((await userdataFileProvider.stat(userdataURIFromPaths([]))).type, FileType.Directory); + await userdataFileProvider.delete(userdataURIFromPaths([]), { recursive: true, useTrash: false }); + assert.strictEqual((await userdataFileProvider.stat(userdataURIFromPaths([]))).type, FileType.Directory); }); test('createFolder', async () => { let event: FileOperationEvent | undefined; disposables.add(service.onDidRunOperation(e => event = e)); - const parent = await service.resolve(makeUserdataURI(testDir)); + const parent = await service.resolve(userdataURIFromPaths([])); + const newFolderResource = joinPath(parent.resource, 'newFolder'); - const newFolderResource = makeUserdataURI(join(parent.resource.path, 'newFolder')); - - assert.equal((await userdataFileProvider.readdir(parent.resource)).length, 0); + assert.strictEqual((await userdataFileProvider.readdir(parent.resource)).length, 0); const newFolder = await service.createFolder(newFolderResource); - assert.equal(newFolder.name, 'newFolder'); - // Invalid.. dirs dont exist in our IDBFSB. - // assert.equal((await userdataFileProvider.readdir(parent.resource)).length, 1); + assert.strictEqual(newFolder.name, 'newFolder'); + assert.strictEqual((await userdataFileProvider.readdir(parent.resource)).length, 1); + assert.strictEqual((await userdataFileProvider.stat(newFolderResource)).type, FileType.Directory); assert.ok(event); - assert.equal(event!.resource.path, newFolderResource.path); - assert.equal(event!.operation, FileOperation.CREATE); - assert.equal(event!.target!.resource.path, newFolderResource.path); - assert.equal(event!.target!.isDirectory, true); + assert.strictEqual(event!.resource.path, newFolderResource.path); + assert.strictEqual(event!.operation, FileOperation.CREATE); + assert.strictEqual(event!.target!.resource.path, newFolderResource.path); + assert.strictEqual(event!.target!.isDirectory, true); + }); + + test('createFolder: creating multiple folders at once', async () => { + let event: FileOperationEvent; + disposables.add(service.onDidRunOperation(e => event = e)); + + const multiFolderPaths = ['a', 'couple', 'of', 'folders']; + const parent = await service.resolve(userdataURIFromPaths([])); + const newFolderResource = joinPath(parent.resource, ...multiFolderPaths); + + const newFolder = await service.createFolder(newFolderResource); + + const lastFolderName = multiFolderPaths[multiFolderPaths.length - 1]; + assert.strictEqual(newFolder.name, lastFolderName); + assert.strictEqual((await userdataFileProvider.stat(newFolderResource)).type, FileType.Directory); + + assert.ok(event!); + assert.strictEqual(event!.resource.path, newFolderResource.path); + assert.strictEqual(event!.operation, FileOperation.CREATE); + assert.strictEqual(event!.target!.resource.path, newFolderResource.path); + assert.strictEqual(event!.target!.isDirectory, true); + }); + + test('exists', async () => { + let exists = await service.exists(userdataURIFromPaths([])); + assert.strictEqual(exists, true); + + exists = await service.exists(userdataURIFromPaths(['hello'])); + assert.strictEqual(exists, false); + }); + + test('resolve - file', async () => { + await initFixtures(); + + const resource = userdataURIFromPaths(['fixtures', 'resolver', 'index.html']); + const resolved = await service.resolve(resource); + + assert.strictEqual(resolved.name, 'index.html'); + assert.strictEqual(resolved.isFile, true); + assert.strictEqual(resolved.isDirectory, false); + assert.strictEqual(resolved.isSymbolicLink, false); + assert.strictEqual(resolved.resource.toString(), resource.toString()); + assert.strictEqual(resolved.children, undefined); + assert.ok(resolved.size! > 0); + }); + + test('resolve - directory', async () => { + await initFixtures(); + + const testsElements = ['examples', 'other', 'index.html', 'site.css']; + + const resource = userdataURIFromPaths(['fixtures', 'resolver']); + const result = await service.resolve(resource); + + assert.ok(result); + assert.strictEqual(result.resource.toString(), resource.toString()); + assert.strictEqual(result.name, 'resolver'); + assert.ok(result.children); + assert.ok(result.children!.length > 0); + assert.ok(result!.isDirectory); + assert.strictEqual(result.children!.length, testsElements.length); + + assert.ok(result.children!.every(entry => { + return testsElements.some(name => { + return basename(entry.resource) === name; + }); + })); + + result.children!.forEach(value => { + assert.ok(basename(value.resource)); + if (['examples', 'other'].indexOf(basename(value.resource)) >= 0) { + assert.ok(value.isDirectory); + assert.strictEqual(value.mtime, undefined); + assert.strictEqual(value.ctime, undefined); + } else if (basename(value.resource) === 'index.html') { + assert.ok(!value.isDirectory); + assert.ok(!value.children); + assert.strictEqual(value.mtime, undefined); + assert.strictEqual(value.ctime, undefined); + } else if (basename(value.resource) === 'site.css') { + assert.ok(!value.isDirectory); + assert.ok(!value.children); + assert.strictEqual(value.mtime, undefined); + assert.strictEqual(value.ctime, undefined); + } else { + assert.ok(!'Unexpected value ' + basename(value.resource)); + } + }); + }); + + test('createFile', async () => { + return assertCreateFile(contents => VSBuffer.fromString(contents)); + }); + + test('createFile (readable)', async () => { + return assertCreateFile(contents => bufferToReadable(VSBuffer.fromString(contents))); + }); + + test('createFile (stream)', async () => { + return assertCreateFile(contents => bufferToStream(VSBuffer.fromString(contents))); + }); + + async function assertCreateFile(converter: (content: string) => VSBuffer | VSBufferReadable | VSBufferReadableStream): Promise { + let event: FileOperationEvent; + disposables.add(service.onDidRunOperation(e => event = e)); + + const contents = 'Hello World'; + const resource = userdataURIFromPaths(['test.txt']); + + assert.strictEqual(await service.canCreateFile(resource), true); + const fileStat = await service.createFile(resource, converter(contents)); + assert.strictEqual(fileStat.name, 'test.txt'); + assert.strictEqual((await userdataFileProvider.stat(fileStat.resource)).type, FileType.File); + assert.strictEqual(new TextDecoder().decode(await userdataFileProvider.readFile(fileStat.resource)), contents); + + assert.ok(event!); + assert.strictEqual(event!.resource.path, resource.path); + assert.strictEqual(event!.operation, FileOperation.CREATE); + assert.strictEqual(event!.target!.resource.path, resource.path); + } + + const makeBatchTester = (size: number, name: string) => { + const batch = Array.from({ length: 50 }).map((_, i) => ({ contents: `Hello${i}`, resource: userdataURIFromPaths(['batched', name, `Hello${i}.txt`]) })); + let stats: Promise | undefined = undefined; + return { + async create() { + return stats = Promise.all(batch.map(entry => service.createFile(entry.resource, VSBuffer.fromString(entry.contents)))); + }, + async assertContentsCorrect() { + await Promise.all(batch.map(async (entry, i) => { + if (!stats) { throw Error('read called before create'); } + const stat = (await stats!)[i]; + assert.strictEqual(stat.name, `Hello${i}.txt`); + assert.strictEqual((await userdataFileProvider.stat(stat.resource)).type, FileType.File); + assert.strictEqual(new TextDecoder().decode(await userdataFileProvider.readFile(stat.resource)), entry.contents); + })); + }, + async delete() { + await service.del(userdataURIFromPaths(['batched', name]), { recursive: true, useTrash: false }); + }, + async assertContentsEmpty() { + if (!stats) { throw Error('assertContentsEmpty called before create'); } + await Promise.all((await stats).map(async stat => { + const newStat = await userdataFileProvider.stat(stat.resource).catch(e => e.code); + assert.strictEqual(newStat, FileSystemProviderErrorCode.FileNotFound); + })); + } + }; + }; + + test('createFile (small batch)', async () => { + const tester = makeBatchTester(50, 'smallBatch'); + await tester.create(); + await tester.assertContentsCorrect(); + await tester.delete(); + await tester.assertContentsEmpty(); + }); + + test('createFile (mixed parallel/sequential)', async () => { + const single1 = makeBatchTester(1, 'single1'); + const single2 = makeBatchTester(1, 'single2'); + + const batch1 = makeBatchTester(20, 'batch1'); + const batch2 = makeBatchTester(20, 'batch2'); + + single1.create(); + batch1.create(); + await Promise.all([single1.assertContentsCorrect(), batch1.assertContentsCorrect()]); + single2.create(); + batch2.create(); + await Promise.all([single2.assertContentsCorrect(), batch2.assertContentsCorrect()]); + await Promise.all([single1.assertContentsCorrect(), batch1.assertContentsCorrect()]); + + await (Promise.all([single1.delete(), single2.delete(), batch1.delete(), batch2.delete()])); + await (Promise.all([single1.assertContentsEmpty(), single2.assertContentsEmpty(), batch1.assertContentsEmpty(), batch2.assertContentsEmpty()])); + }); + + test('deleteFile', async () => { + await initFixtures(); + + let event: FileOperationEvent; + disposables.add(service.onDidRunOperation(e => event = e)); + + const anotherResource = userdataURIFromPaths(['fixtures', 'service', 'deep', 'company.js']); + const resource = userdataURIFromPaths(['fixtures', 'service', 'deep', 'conway.js']); + const source = await service.resolve(resource); + + assert.strictEqual(await service.canDelete(source.resource, { useTrash: false }), true); + await service.del(source.resource, { useTrash: false }); + + assert.strictEqual(await service.exists(source.resource), false); + assert.strictEqual(await service.exists(anotherResource), true); + + assert.ok(event!); + assert.strictEqual(event!.resource.path, resource.path); + assert.strictEqual(event!.operation, FileOperation.DELETE); + + { + let error: Error | undefined = undefined; + try { + await service.del(source.resource, { useTrash: false }); + } catch (e) { + error = e; + } + + assert.ok(error); + assert.strictEqual((error).fileOperationResult, FileOperationResult.FILE_NOT_FOUND); + } + await reload(); + { + let error: Error | undefined = undefined; + try { + await service.del(source.resource, { useTrash: false }); + } catch (e) { + error = e; + } + + assert.ok(error); + assert.strictEqual((error).fileOperationResult, FileOperationResult.FILE_NOT_FOUND); + } + }); + + test('deleteFolder (recursive)', async () => { + await initFixtures(); + let event: FileOperationEvent; + disposables.add(service.onDidRunOperation(e => event = e)); + + const resource = userdataURIFromPaths(['fixtures', 'service', 'deep']); + const subResource1 = userdataURIFromPaths(['fixtures', 'service', 'deep', 'company.js']); + const subResource2 = userdataURIFromPaths(['fixtures', 'service', 'deep', 'conway.js']); + assert.strictEqual(await service.exists(subResource1), true); + assert.strictEqual(await service.exists(subResource2), true); + + const source = await service.resolve(resource); + + assert.strictEqual(await service.canDelete(source.resource, { recursive: true, useTrash: false }), true); + await service.del(source.resource, { recursive: true, useTrash: false }); + + assert.strictEqual(await service.exists(source.resource), false); + assert.strictEqual(await service.exists(subResource1), false); + assert.strictEqual(await service.exists(subResource2), false); + assert.ok(event!); + assert.strictEqual(event!.resource.fsPath, resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.DELETE); + }); + + + test('deleteFolder (non recursive)', async () => { + await initFixtures(); + const resource = userdataURIFromPaths(['fixtures', 'service', 'deep']); + const source = await service.resolve(resource); + + assert.ok((await service.canDelete(source.resource)) instanceof Error); + + let error; + try { + await service.del(source.resource); + } catch (e) { + error = e; + } + assert.ok(error); }); }); diff --git a/src/vs/platform/files/test/electron-browser/diskFileService.test.ts b/src/vs/platform/files/test/electron-browser/diskFileService.test.ts index 4f7a0b94..4c8f13fc 100644 --- a/src/vs/platform/files/test/electron-browser/diskFileService.test.ts +++ b/src/vs/platform/files/test/electron-browser/diskFileService.test.ts @@ -8,11 +8,10 @@ import { tmpdir } from 'os'; import { FileService } from 'vs/platform/files/common/fileService'; import { Schemas } from 'vs/base/common/network'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; -import { getRandomTestPath } from 'vs/base/test/node/testUtils'; -import { generateUuid } from 'vs/base/common/uuid'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { join, basename, dirname, posix } from 'vs/base/common/path'; import { getPathFromAmdModule } from 'vs/base/common/amd'; -import { copy, rimraf, symlink, RimRafMode, rimrafSync } from 'vs/base/node/pfs'; +import { copy, rimraf, symlink, rimrafSync } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, createReadStream } from 'fs'; import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities, FileChangeType, IFileChange, FileChangesEvent, FileOperationError, etag, IStat, IFileStatWithMetadata } from 'vs/platform/files/common/files'; @@ -118,26 +117,18 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider { } } -suite('Disk File Service', function () { +flakySuite('Disk File Service', function () { - const parentDir = getRandomTestPath(tmpdir(), 'vsctests', 'diskfileservice'); const testSchema = 'test'; let service: FileService; let fileProvider: TestDiskFileSystemProvider; let testProvider: TestDiskFileSystemProvider; + let testDir: string; const disposables = new DisposableStore(); - // Given issues such as https://github.com/microsoft/vscode/issues/78602 - // and https://github.com/microsoft/vscode/issues/92334 we see random test - // failures when accessing the native file system. To diagnose further, we - // retry node.js file access tests up to 3 times to rule out any random disk - // issue and increase the timeout. - this.retries(3); - this.timeout(1000 * 10); - setup(async () => { const logService = new NullLogService(); @@ -152,17 +143,17 @@ suite('Disk File Service', function () { disposables.add(service.registerProvider(testSchema, testProvider)); disposables.add(testProvider); - const id = generateUuid(); - testDir = join(parentDir, id); + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'diskfileservice'); + const sourceDir = getPathFromAmdModule(require, './fixtures/service'); await copy(sourceDir, testDir); }); - teardown(async () => { + teardown(() => { disposables.clear(); - await rimraf(parentDir, RimRafMode.MOVE); + return rimraf(testDir); }); test('createFolder', async () => { @@ -175,14 +166,14 @@ suite('Disk File Service', function () { const newFolder = await service.createFolder(newFolderResource); - assert.equal(newFolder.name, 'newFolder'); - assert.equal(existsSync(newFolder.resource.fsPath), true); + assert.strictEqual(newFolder.name, 'newFolder'); + assert.strictEqual(existsSync(newFolder.resource.fsPath), true); assert.ok(event); - assert.equal(event!.resource.fsPath, newFolderResource.fsPath); - assert.equal(event!.operation, FileOperation.CREATE); - assert.equal(event!.target!.resource.fsPath, newFolderResource.fsPath); - assert.equal(event!.target!.isDirectory, true); + assert.strictEqual(event!.resource.fsPath, newFolderResource.fsPath); + assert.strictEqual(event!.operation, FileOperation.CREATE); + assert.strictEqual(event!.target!.resource.fsPath, newFolderResource.fsPath); + assert.strictEqual(event!.target!.isDirectory, true); }); test('createFolder: creating multiple folders at once', async () => { @@ -197,34 +188,34 @@ suite('Disk File Service', function () { const newFolder = await service.createFolder(newFolderResource); const lastFolderName = multiFolderPaths[multiFolderPaths.length - 1]; - assert.equal(newFolder.name, lastFolderName); - assert.equal(existsSync(newFolder.resource.fsPath), true); + assert.strictEqual(newFolder.name, lastFolderName); + assert.strictEqual(existsSync(newFolder.resource.fsPath), true); assert.ok(event!); - assert.equal(event!.resource.fsPath, newFolderResource.fsPath); - assert.equal(event!.operation, FileOperation.CREATE); - assert.equal(event!.target!.resource.fsPath, newFolderResource.fsPath); - assert.equal(event!.target!.isDirectory, true); + assert.strictEqual(event!.resource.fsPath, newFolderResource.fsPath); + assert.strictEqual(event!.operation, FileOperation.CREATE); + assert.strictEqual(event!.target!.resource.fsPath, newFolderResource.fsPath); + assert.strictEqual(event!.target!.isDirectory, true); }); test('exists', async () => { let exists = await service.exists(URI.file(testDir)); - assert.equal(exists, true); + assert.strictEqual(exists, true); exists = await service.exists(URI.file(testDir + 'something')); - assert.equal(exists, false); + assert.strictEqual(exists, false); }); test('resolve - file', async () => { const resource = URI.file(getPathFromAmdModule(require, './fixtures/resolver/index.html')); const resolved = await service.resolve(resource); - assert.equal(resolved.name, 'index.html'); - assert.equal(resolved.isFile, true); - assert.equal(resolved.isDirectory, false); - assert.equal(resolved.isSymbolicLink, false); - assert.equal(resolved.resource.toString(), resource.toString()); - assert.equal(resolved.children, undefined); + assert.strictEqual(resolved.name, 'index.html'); + assert.strictEqual(resolved.isFile, true); + assert.strictEqual(resolved.isDirectory, false); + assert.strictEqual(resolved.isSymbolicLink, false); + assert.strictEqual(resolved.resource.toString(), resource.toString()); + assert.strictEqual(resolved.children, undefined); assert.ok(resolved.mtime! > 0); assert.ok(resolved.ctime! > 0); assert.ok(resolved.size! > 0); @@ -237,14 +228,14 @@ suite('Disk File Service', function () { const result = await service.resolve(resource); assert.ok(result); - assert.equal(result.resource.toString(), resource.toString()); - assert.equal(result.name, 'resolver'); + assert.strictEqual(result.resource.toString(), resource.toString()); + assert.strictEqual(result.name, 'resolver'); assert.ok(result.children); assert.ok(result.children!.length > 0); assert.ok(result!.isDirectory); assert.ok(result.mtime! > 0); assert.ok(result.ctime! > 0); - assert.equal(result.children!.length, testsElements.length); + assert.strictEqual(result.children!.length, testsElements.length); assert.ok(result.children!.every(entry => { return testsElements.some(name => { @@ -256,18 +247,18 @@ suite('Disk File Service', function () { assert.ok(basename(value.resource.fsPath)); if (['examples', 'other'].indexOf(basename(value.resource.fsPath)) >= 0) { assert.ok(value.isDirectory); - assert.equal(value.mtime, undefined); - assert.equal(value.ctime, undefined); + assert.strictEqual(value.mtime, undefined); + assert.strictEqual(value.ctime, undefined); } else if (basename(value.resource.fsPath) === 'index.html') { assert.ok(!value.isDirectory); assert.ok(!value.children); - assert.equal(value.mtime, undefined); - assert.equal(value.ctime, undefined); + assert.strictEqual(value.mtime, undefined); + assert.strictEqual(value.ctime, undefined); } else if (basename(value.resource.fsPath) === 'site.css') { assert.ok(!value.isDirectory); assert.ok(!value.children); - assert.equal(value.mtime, undefined); - assert.equal(value.ctime, undefined); + assert.strictEqual(value.mtime, undefined); + assert.strictEqual(value.ctime, undefined); } else { assert.ok(!'Unexpected value ' + basename(value.resource.fsPath)); } @@ -280,13 +271,13 @@ suite('Disk File Service', function () { const result = await service.resolve(URI.file(getPathFromAmdModule(require, './fixtures/resolver')), { resolveMetadata: true }); assert.ok(result); - assert.equal(result.name, 'resolver'); + assert.strictEqual(result.name, 'resolver'); assert.ok(result.children); assert.ok(result.children!.length > 0); assert.ok(result!.isDirectory); assert.ok(result.mtime! > 0); assert.ok(result.ctime! > 0); - assert.equal(result.children!.length, testsElements.length); + assert.strictEqual(result.children!.length, testsElements.length); assert.ok(result.children!.every(entry => { return testsElements.some(name => { @@ -320,10 +311,10 @@ suite('Disk File Service', function () { test('resolve - directory with resolveTo', async () => { const resolved = await service.resolve(URI.file(testDir), { resolveTo: [URI.file(join(testDir, 'deep'))] }); - assert.equal(resolved.children!.length, 8); + assert.strictEqual(resolved.children!.length, 8); const deep = (getByName(resolved, 'deep')!); - assert.equal(deep.children!.length, 4); + assert.strictEqual(deep.children!.length, 4); }); test('resolve - directory - resolveTo single directory', async () => { @@ -336,7 +327,7 @@ suite('Disk File Service', function () { assert.ok(result.isDirectory); const children = result.children!; - assert.equal(children.length, 4); + assert.strictEqual(children.length, 4); const other = getByName(result, 'other'); assert.ok(other); @@ -345,7 +336,7 @@ suite('Disk File Service', function () { const deep = getByName(other!, 'deep'); assert.ok(deep); assert.ok(deep!.children!.length > 0); - assert.equal(deep!.children!.length, 4); + assert.strictEqual(deep!.children!.length, 4); }); test('resolve directory - resolveTo multiple directories', async () => { @@ -363,7 +354,7 @@ suite('Disk File Service', function () { assert.ok(result.isDirectory); const children = result.children!; - assert.equal(children.length, 4); + assert.strictEqual(children.length, 4); const other = getByName(result, 'other'); assert.ok(other); @@ -372,12 +363,12 @@ suite('Disk File Service', function () { const deep = getByName(other!, 'deep'); assert.ok(deep); assert.ok(deep!.children!.length > 0); - assert.equal(deep!.children!.length, 4); + assert.strictEqual(deep!.children!.length, 4); const examples = getByName(result, 'examples'); assert.ok(examples); assert.ok(examples!.children!.length > 0); - assert.equal(examples!.children!.length, 4); + assert.strictEqual(examples!.children!.length, 4); }); test('resolve directory - resolveSingleChildFolders', async () => { @@ -390,12 +381,12 @@ suite('Disk File Service', function () { assert.ok(result.isDirectory); const children = result.children!; - assert.equal(children.length, 1); + assert.strictEqual(children.length, 1); let deep = getByName(result, 'deep'); assert.ok(deep); assert.ok(deep!.children!.length > 0); - assert.equal(deep!.children!.length, 4); + assert.strictEqual(deep!.children!.length, 4); }); test('resolves', async () => { @@ -405,41 +396,41 @@ suite('Disk File Service', function () { ]); const r1 = (res[0].stat!); - assert.equal(r1.children!.length, 8); + assert.strictEqual(r1.children!.length, 8); const deep = (getByName(r1, 'deep')!); - assert.equal(deep.children!.length, 4); + assert.strictEqual(deep.children!.length, 4); const r2 = (res[1].stat!); - assert.equal(r2.children!.length, 4); - assert.equal(r2.name, 'deep'); + assert.strictEqual(r2.children!.length, 4); + assert.strictEqual(r2.name, 'deep'); }); - (isWindows /* not reliable on windows */ ? test.skip : test)('resolve - folder symbolic link', async () => { + test('resolve - folder symbolic link', async () => { const link = URI.file(join(testDir, 'deep-link')); - await symlink(join(testDir, 'deep'), link.fsPath); + await symlink(join(testDir, 'deep'), link.fsPath, 'junction'); const resolved = await service.resolve(link); - assert.equal(resolved.children!.length, 4); - assert.equal(resolved.isDirectory, true); - assert.equal(resolved.isSymbolicLink, true); + assert.strictEqual(resolved.children!.length, 4); + assert.strictEqual(resolved.isDirectory, true); + assert.strictEqual(resolved.isSymbolicLink, true); }); - (isWindows /* not reliable on windows */ ? test.skip : test)('resolve - file symbolic link', async () => { + (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('resolve - file symbolic link', async () => { const link = URI.file(join(testDir, 'lorem.txt-linked')); await symlink(join(testDir, 'lorem.txt'), link.fsPath); const resolved = await service.resolve(link); - assert.equal(resolved.isDirectory, false); - assert.equal(resolved.isSymbolicLink, true); + assert.strictEqual(resolved.isDirectory, false); + assert.strictEqual(resolved.isSymbolicLink, true); }); - (isWindows /* not reliable on windows */ ? test.skip : test)('resolve - symbolic link pointing to non-existing file does not break', async () => { - await symlink(join(testDir, 'foo'), join(testDir, 'bar')); + test('resolve - symbolic link pointing to non-existing file does not break', async () => { + await symlink(join(testDir, 'foo'), join(testDir, 'bar'), 'junction'); const resolved = await service.resolve(URI.file(testDir)); - assert.equal(resolved.isDirectory, true); - assert.equal(resolved.children!.length, 9); + assert.strictEqual(resolved.isDirectory, true); + assert.strictEqual(resolved.children!.length, 9); const resolvedLink = resolved.children?.find(child => child.name === 'bar' && child.isSymbolicLink); assert.ok(resolvedLink); @@ -463,14 +454,14 @@ suite('Disk File Service', function () { const resource = URI.file(join(testDir, 'deep', 'conway.js')); const source = await service.resolve(resource); - assert.equal(await service.canDelete(source.resource, { useTrash }), true); + assert.strictEqual(await service.canDelete(source.resource, { useTrash }), true); await service.del(source.resource, { useTrash }); - assert.equal(existsSync(source.resource.fsPath), false); + assert.strictEqual(existsSync(source.resource.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, resource.fsPath); - assert.equal(event!.operation, FileOperation.DELETE); + assert.strictEqual(event!.resource.fsPath, resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.DELETE); let error: Error | undefined = undefined; try { @@ -480,10 +471,10 @@ suite('Disk File Service', function () { } assert.ok(error); - assert.equal((error).fileOperationResult, FileOperationResult.FILE_NOT_FOUND); + assert.strictEqual((error).fileOperationResult, FileOperationResult.FILE_NOT_FOUND); } - (isWindows /* not reliable on windows */ ? test.skip : test)('deleteFile - symbolic link (exists)', async () => { + (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (exists)', async () => { const target = URI.file(join(testDir, 'lorem.txt')); const link = URI.file(join(testDir, 'lorem.txt-linked')); await symlink(target.fsPath, link.fsPath); @@ -493,19 +484,19 @@ suite('Disk File Service', function () { let event: FileOperationEvent; disposables.add(service.onDidRunOperation(e => event = e)); - assert.equal(await service.canDelete(source.resource), true); + assert.strictEqual(await service.canDelete(source.resource), true); await service.del(source.resource); - assert.equal(existsSync(source.resource.fsPath), false); + assert.strictEqual(existsSync(source.resource.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, link.fsPath); - assert.equal(event!.operation, FileOperation.DELETE); + assert.strictEqual(event!.resource.fsPath, link.fsPath); + assert.strictEqual(event!.operation, FileOperation.DELETE); - assert.equal(existsSync(target.fsPath), true); // target the link pointed to is never deleted + assert.strictEqual(existsSync(target.fsPath), true); // target the link pointed to is never deleted }); - (isWindows /* not reliable on windows */ ? test.skip : test)('deleteFile - symbolic link (pointing to non-existing file)', async () => { + (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (pointing to non-existing file)', async () => { const target = URI.file(join(testDir, 'foo')); const link = URI.file(join(testDir, 'bar')); await symlink(target.fsPath, link.fsPath); @@ -513,14 +504,14 @@ suite('Disk File Service', function () { let event: FileOperationEvent; disposables.add(service.onDidRunOperation(e => event = e)); - assert.equal(await service.canDelete(link), true); + assert.strictEqual(await service.canDelete(link), true); await service.del(link); - assert.equal(existsSync(link.fsPath), false); + assert.strictEqual(existsSync(link.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, link.fsPath); - assert.equal(event!.operation, FileOperation.DELETE); + assert.strictEqual(event!.resource.fsPath, link.fsPath); + assert.strictEqual(event!.operation, FileOperation.DELETE); }); test('deleteFolder (recursive)', async () => { @@ -538,13 +529,13 @@ suite('Disk File Service', function () { const resource = URI.file(join(testDir, 'deep')); const source = await service.resolve(resource); - assert.equal(await service.canDelete(source.resource, { recursive: true, useTrash }), true); + assert.strictEqual(await service.canDelete(source.resource, { recursive: true, useTrash }), true); await service.del(source.resource, { recursive: true, useTrash }); - assert.equal(existsSync(source.resource.fsPath), false); + assert.strictEqual(existsSync(source.resource.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, resource.fsPath); - assert.equal(event!.operation, FileOperation.DELETE); + assert.strictEqual(event!.resource.fsPath, resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.DELETE); } test('deleteFolder (non recursive)', async () => { @@ -572,20 +563,20 @@ suite('Disk File Service', function () { const target = URI.file(join(dirname(source.fsPath), 'other.html')); - assert.equal(await service.canMove(source, target), true); + assert.strictEqual(await service.canMove(source, target), true); const renamed = await service.move(source, target); - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(existsSync(source.fsPath), false); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(existsSync(source.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.fsPath); + assert.strictEqual(event!.operation, FileOperation.MOVE); + assert.strictEqual(event!.target!.resource.fsPath, renamed.resource.fsPath); const targetContents = readFileSync(target.fsPath); - assert.equal(sourceContents.byteLength, targetContents.byteLength); - assert.equal(sourceContents.toString(), targetContents.toString()); + assert.strictEqual(sourceContents.byteLength, targetContents.byteLength); + assert.strictEqual(sourceContents.toString(), targetContents.toString()); }); test('move - across providers (buffered => buffered)', async () => { @@ -653,20 +644,20 @@ suite('Disk File Service', function () { const target = URI.file(join(dirname(source.fsPath), 'other.html')).with({ scheme: testSchema }); - assert.equal(await service.canMove(source, target), true); + assert.strictEqual(await service.canMove(source, target), true); const renamed = await service.move(source, target); - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(existsSync(source.fsPath), false); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(existsSync(source.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.fsPath); - assert.equal(event!.operation, FileOperation.COPY); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.fsPath); + assert.strictEqual(event!.operation, FileOperation.COPY); + assert.strictEqual(event!.target!.resource.fsPath, renamed.resource.fsPath); const targetContents = readFileSync(target.fsPath); - assert.equal(sourceContents.byteLength, targetContents.byteLength); - assert.equal(sourceContents.toString(), targetContents.toString()); + assert.strictEqual(sourceContents.byteLength, targetContents.byteLength); + assert.strictEqual(sourceContents.toString(), targetContents.toString()); } test('move - multi folder', async () => { @@ -678,15 +669,15 @@ suite('Disk File Service', function () { const source = URI.file(join(testDir, 'index.html')); - assert.equal(await service.canMove(source, URI.file(join(dirname(source.fsPath), renameToPath))), true); + assert.strictEqual(await service.canMove(source, URI.file(join(dirname(source.fsPath), renameToPath))), true); const renamed = await service.move(source, URI.file(join(dirname(source.fsPath), renameToPath))); - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(existsSync(source.fsPath), false); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(existsSync(source.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.fsPath); + assert.strictEqual(event!.operation, FileOperation.MOVE); + assert.strictEqual(event!.target!.resource.fsPath, renamed.resource.fsPath); }); test('move - directory', async () => { @@ -695,15 +686,15 @@ suite('Disk File Service', function () { const source = URI.file(join(testDir, 'deep')); - assert.equal(await service.canMove(source, URI.file(join(dirname(source.fsPath), 'deeper'))), true); + assert.strictEqual(await service.canMove(source, URI.file(join(dirname(source.fsPath), 'deeper'))), true); const renamed = await service.move(source, URI.file(join(dirname(source.fsPath), 'deeper'))); - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(existsSync(source.fsPath), false); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(existsSync(source.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.fsPath); + assert.strictEqual(event!.operation, FileOperation.MOVE); + assert.strictEqual(event!.target!.resource.fsPath, renamed.resource.fsPath); }); test('move - directory - across providers (buffered => buffered)', async () => { @@ -743,20 +734,20 @@ suite('Disk File Service', function () { const target = URI.file(join(dirname(source.fsPath), 'deeper')).with({ scheme: testSchema }); - assert.equal(await service.canMove(source, target), true); + assert.strictEqual(await service.canMove(source, target), true); const renamed = await service.move(source, target); - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(existsSync(source.fsPath), false); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(existsSync(source.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.fsPath); - assert.equal(event!.operation, FileOperation.COPY); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.fsPath); + assert.strictEqual(event!.operation, FileOperation.COPY); + assert.strictEqual(event!.target!.resource.fsPath, renamed.resource.fsPath); const targetChildren = readdirSync(target.fsPath); - assert.equal(sourceChildren.length, targetChildren.length); + assert.strictEqual(sourceChildren.length, targetChildren.length); for (let i = 0; i < sourceChildren.length; i++) { - assert.equal(sourceChildren[i], targetChildren[i]); + assert.strictEqual(sourceChildren[i], targetChildren[i]); } } @@ -768,18 +759,18 @@ suite('Disk File Service', function () { assert.ok(source.size > 0); const renamedResource = URI.file(join(dirname(source.resource.fsPath), 'INDEX.html')); - assert.equal(await service.canMove(source.resource, renamedResource), true); + assert.strictEqual(await service.canMove(source.resource, renamedResource), true); let renamed = await service.move(source.resource, renamedResource); - assert.equal(existsSync(renamedResource.fsPath), true); - assert.equal(basename(renamedResource.fsPath), 'INDEX.html'); + assert.strictEqual(existsSync(renamedResource.fsPath), true); + assert.strictEqual(basename(renamedResource.fsPath), 'INDEX.html'); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.resource.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamedResource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.MOVE); + assert.strictEqual(event!.target!.resource.fsPath, renamedResource.fsPath); renamed = await service.resolve(renamedResource, { resolveMetadata: true }); - assert.equal(source.size, renamed.size); + assert.strictEqual(source.size, renamed.size); }); test('move - same file', async () => { @@ -789,18 +780,18 @@ suite('Disk File Service', function () { const source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true }); assert.ok(source.size > 0); - assert.equal(await service.canMove(source.resource, URI.file(source.resource.fsPath)), true); + assert.strictEqual(await service.canMove(source.resource, URI.file(source.resource.fsPath)), true); let renamed = await service.move(source.resource, URI.file(source.resource.fsPath)); - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(basename(renamed.resource.fsPath), 'index.html'); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(basename(renamed.resource.fsPath), 'index.html'); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.resource.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.MOVE); + assert.strictEqual(event!.target!.resource.fsPath, renamed.resource.fsPath); renamed = await service.resolve(renamed.resource, { resolveMetadata: true }); - assert.equal(source.size, renamed.size); + assert.strictEqual(source.size, renamed.size); }); test('move - same file #2', async () => { @@ -813,18 +804,18 @@ suite('Disk File Service', function () { const targetParent = URI.file(testDir); const target = targetParent.with({ path: posix.join(targetParent.path, posix.basename(source.resource.path)) }); - assert.equal(await service.canMove(source.resource, target), true); + assert.strictEqual(await service.canMove(source.resource, target), true); let renamed = await service.move(source.resource, target); - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(basename(renamed.resource.fsPath), 'index.html'); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(basename(renamed.resource.fsPath), 'index.html'); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.resource.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.MOVE); + assert.strictEqual(event!.target!.resource.fsPath, renamed.resource.fsPath); renamed = await service.resolve(renamed.resource, { resolveMetadata: true }); - assert.equal(source.size, renamed.size); + assert.strictEqual(source.size, renamed.size); }); test('move - source parent of target', async () => { @@ -848,7 +839,7 @@ suite('Disk File Service', function () { assert.ok(!event!); source = await service.resolve(source.resource, { resolveMetadata: true }); - assert.equal(originalSize, source.size); + assert.strictEqual(originalSize, source.size); }); test('move - FILE_MOVE_CONFLICT', async () => { @@ -868,11 +859,11 @@ suite('Disk File Service', function () { error = e; } - assert.equal(error.fileOperationResult, FileOperationResult.FILE_MOVE_CONFLICT); + assert.strictEqual(error.fileOperationResult, FileOperationResult.FILE_MOVE_CONFLICT); assert.ok(!event!); source = await service.resolve(source.resource, { resolveMetadata: true }); - assert.equal(originalSize, source.size); + assert.strictEqual(originalSize, source.size); }); test('move - overwrite folder with file', async () => { @@ -894,17 +885,17 @@ suite('Disk File Service', function () { const f = await service.createFolder(folderResource); const source = URI.file(join(testDir, 'deep', 'conway.js')); - assert.equal(await service.canMove(source, f.resource, true), true); + assert.strictEqual(await service.canMove(source, f.resource, true), true); const moved = await service.move(source, f.resource, true); - assert.equal(existsSync(moved.resource.fsPath), true); + assert.strictEqual(existsSync(moved.resource.fsPath), true); assert.ok(statSync(moved.resource.fsPath).isFile); assert.ok(createEvent!); assert.ok(deleteEvent!); assert.ok(moveEvent!); - assert.equal(moveEvent!.resource.fsPath, source.fsPath); - assert.equal(moveEvent!.target!.resource.fsPath, moved.resource.fsPath); - assert.equal(deleteEvent!.resource.fsPath, folderResource.fsPath); + assert.strictEqual(moveEvent!.resource.fsPath, source.fsPath); + assert.strictEqual(moveEvent!.target!.resource.fsPath, moved.resource.fsPath); + assert.strictEqual(deleteEvent!.resource.fsPath, folderResource.fsPath); }); test('copy', async () => { @@ -949,21 +940,21 @@ suite('Disk File Service', function () { const source = await service.resolve(URI.file(join(testDir, sourceName))); const target = URI.file(join(testDir, 'other.html')); - assert.equal(await service.canCopy(source.resource, target), true); + assert.strictEqual(await service.canCopy(source.resource, target), true); const copied = await service.copy(source.resource, target); - assert.equal(existsSync(copied.resource.fsPath), true); - assert.equal(existsSync(source.resource.fsPath), true); + assert.strictEqual(existsSync(copied.resource.fsPath), true); + assert.strictEqual(existsSync(source.resource.fsPath), true); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.resource.fsPath); - assert.equal(event!.operation, FileOperation.COPY); - assert.equal(event!.target!.resource.fsPath, copied.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.COPY); + assert.strictEqual(event!.target!.resource.fsPath, copied.resource.fsPath); const sourceContents = readFileSync(source.resource.fsPath); const targetContents = readFileSync(target.fsPath); - assert.equal(sourceContents.byteLength, targetContents.byteLength); - assert.equal(sourceContents.toString(), targetContents.toString()); + assert.strictEqual(sourceContents.byteLength, targetContents.byteLength); + assert.strictEqual(sourceContents.toString(), targetContents.toString()); } test('copy - overwrite folder with file', async () => { @@ -985,17 +976,17 @@ suite('Disk File Service', function () { const f = await service.createFolder(folderResource); const source = URI.file(join(testDir, 'deep', 'conway.js')); - assert.equal(await service.canCopy(source, f.resource, true), true); + assert.strictEqual(await service.canCopy(source, f.resource, true), true); const copied = await service.copy(source, f.resource, true); - assert.equal(existsSync(copied.resource.fsPath), true); + assert.strictEqual(existsSync(copied.resource.fsPath), true); assert.ok(statSync(copied.resource.fsPath).isFile); assert.ok(createEvent!); assert.ok(deleteEvent!); assert.ok(copyEvent!); - assert.equal(copyEvent!.resource.fsPath, source.fsPath); - assert.equal(copyEvent!.target!.resource.fsPath, copied.resource.fsPath); - assert.equal(deleteEvent!.resource.fsPath, folderResource.fsPath); + assert.strictEqual(copyEvent!.resource.fsPath, source.fsPath); + assert.strictEqual(copyEvent!.target!.resource.fsPath, copied.resource.fsPath); + assert.strictEqual(deleteEvent!.resource.fsPath, folderResource.fsPath); }); test('copy - MIX CASE same target - no overwrite', async () => { @@ -1017,17 +1008,17 @@ suite('Disk File Service', function () { if (isLinux) { assert.ok(!error); - assert.equal(canCopy, true); + assert.strictEqual(canCopy, true); - assert.equal(existsSync(copied!.resource.fsPath), true); + assert.strictEqual(existsSync(copied!.resource.fsPath), true); assert.ok(readdirSync(testDir).some(f => f === 'INDEX.html')); - assert.equal(source.size, copied!.size); + assert.strictEqual(source.size, copied!.size); } else { assert.ok(error); assert.ok(canCopy instanceof Error); source = await service.resolve(source.resource, { resolveMetadata: true }); - assert.equal(originalSize, source.size); + assert.strictEqual(originalSize, source.size); } }); @@ -1050,17 +1041,17 @@ suite('Disk File Service', function () { if (isLinux) { assert.ok(!error); - assert.equal(canCopy, true); + assert.strictEqual(canCopy, true); - assert.equal(existsSync(copied!.resource.fsPath), true); + assert.strictEqual(existsSync(copied!.resource.fsPath), true); assert.ok(readdirSync(testDir).some(f => f === 'INDEX.html')); - assert.equal(source.size, copied!.size); + assert.strictEqual(source.size, copied!.size); } else { assert.ok(error); assert.ok(canCopy instanceof Error); source = await service.resolve(source.resource, { resolveMetadata: true }); - assert.equal(originalSize, source.size); + assert.strictEqual(originalSize, source.size); } }); @@ -1069,18 +1060,18 @@ suite('Disk File Service', function () { assert.ok(source1.size > 0); const renamed = await service.move(source1.resource, URI.file(join(dirname(source1.resource.fsPath), 'CONWAY.js'))); - assert.equal(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); assert.ok(readdirSync(testDir).some(f => f === 'CONWAY.js')); - assert.equal(source1.size, renamed.size); + assert.strictEqual(source1.size, renamed.size); const source2 = await service.resolve(URI.file(join(testDir, 'deep', 'conway.js')), { resolveMetadata: true }); const target = URI.file(join(testDir, basename(source2.resource.path))); - assert.equal(await service.canCopy(source2.resource, target, true), true); + assert.strictEqual(await service.canCopy(source2.resource, target, true), true); const res = await service.copy(source2.resource, target, true); - assert.equal(existsSync(res.resource.fsPath), true); + assert.strictEqual(existsSync(res.resource.fsPath), true); assert.ok(readdirSync(testDir).some(f => f === 'conway.js')); - assert.equal(source2.size, res.size); + assert.strictEqual(source2.size, res.size); }); test('copy - same file', async () => { @@ -1090,18 +1081,18 @@ suite('Disk File Service', function () { const source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true }); assert.ok(source.size > 0); - assert.equal(await service.canCopy(source.resource, URI.file(source.resource.fsPath)), true); + assert.strictEqual(await service.canCopy(source.resource, URI.file(source.resource.fsPath)), true); let copied = await service.copy(source.resource, URI.file(source.resource.fsPath)); - assert.equal(existsSync(copied.resource.fsPath), true); - assert.equal(basename(copied.resource.fsPath), 'index.html'); + assert.strictEqual(existsSync(copied.resource.fsPath), true); + assert.strictEqual(basename(copied.resource.fsPath), 'index.html'); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.resource.fsPath); - assert.equal(event!.operation, FileOperation.COPY); - assert.equal(event!.target!.resource.fsPath, copied.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.COPY); + assert.strictEqual(event!.target!.resource.fsPath, copied.resource.fsPath); copied = await service.resolve(source.resource, { resolveMetadata: true }); - assert.equal(source.size, copied.size); + assert.strictEqual(source.size, copied.size); }); test('copy - same file #2', async () => { @@ -1114,18 +1105,18 @@ suite('Disk File Service', function () { const targetParent = URI.file(testDir); const target = targetParent.with({ path: posix.join(targetParent.path, posix.basename(source.resource.path)) }); - assert.equal(await service.canCopy(source.resource, URI.file(target.fsPath)), true); + assert.strictEqual(await service.canCopy(source.resource, URI.file(target.fsPath)), true); let copied = await service.copy(source.resource, URI.file(target.fsPath)); - assert.equal(existsSync(copied.resource.fsPath), true); - assert.equal(basename(copied.resource.fsPath), 'index.html'); + assert.strictEqual(existsSync(copied.resource.fsPath), true); + assert.strictEqual(basename(copied.resource.fsPath), 'index.html'); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.resource.fsPath); - assert.equal(event!.operation, FileOperation.COPY); - assert.equal(event!.target!.resource.fsPath, copied.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.COPY); + assert.strictEqual(event!.target!.resource.fsPath, copied.resource.fsPath); copied = await service.resolve(source.resource, { resolveMetadata: true }); - assert.equal(source.size, copied.size); + assert.strictEqual(source.size, copied.size); }); test('readFile - small file - default', () => { @@ -1193,7 +1184,7 @@ suite('Disk File Service', function () { async function testReadFile(resource: URI): Promise { const content = await service.readFile(resource); - assert.equal(content.value.toString(), readFileSync(resource.fsPath)); + assert.strictEqual(content.value.toString(), readFileSync(resource.fsPath).toString()); } test('readFileStream - small file - default', () => { @@ -1221,7 +1212,7 @@ suite('Disk File Service', function () { async function testReadFileStream(resource: URI): Promise { const content = await service.readFileStream(resource); - assert.equal((await streamToBuffer(content.value)).toString(), readFileSync(resource.fsPath)); + assert.strictEqual((await streamToBuffer(content.value)).toString(), readFileSync(resource.fsPath).toString()); } test('readFile - Files are intermingled #38331 - default', async () => { @@ -1260,8 +1251,8 @@ suite('Disk File Service', function () { service.readFile(resource2) ]); - assert.equal(result[0].value.toString(), value1.value.toString()); - assert.equal(result[1].value.toString(), value2.value.toString()); + assert.strictEqual(result[0].value.toString(), value1.value.toString()); + assert.strictEqual(result[1].value.toString(), value2.value.toString()); } test('readFile - from position (ASCII) - default', async () => { @@ -1291,7 +1282,7 @@ suite('Disk File Service', function () { const contents = await service.readFile(resource, { position: 6 }); - assert.equal(contents.value.toString(), 'File'); + assert.strictEqual(contents.value.toString(), 'File'); } test('readFile - from position (with umlaut) - default', async () => { @@ -1321,7 +1312,7 @@ suite('Disk File Service', function () { const contents = await service.readFile(resource, { position: Buffer.from('Small File with Ü').length }); - assert.equal(contents.value.toString(), 'mlaut'); + assert.strictEqual(contents.value.toString(), 'mlaut'); } test('readFile - 3 bytes (ASCII) - default', async () => { @@ -1351,7 +1342,7 @@ suite('Disk File Service', function () { const contents = await service.readFile(resource, { length: 3 }); - assert.equal(contents.value.toString(), 'Sma'); + assert.strictEqual(contents.value.toString(), 'Sma'); } test('readFile - 20000 bytes (large) - default', async () => { @@ -1403,7 +1394,7 @@ suite('Disk File Service', function () { const contents = await service.readFile(resource, { length }); - assert.equal(contents.value.byteLength, length); + assert.strictEqual(contents.value.byteLength, length); } test('readFile - FILE_IS_DIRECTORY', async () => { @@ -1417,7 +1408,7 @@ suite('Disk File Service', function () { } assert.ok(error); - assert.equal(error!.fileOperationResult, FileOperationResult.FILE_IS_DIRECTORY); + assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_IS_DIRECTORY); }); (isWindows /* error code does not seem to be supported on windows */ ? test.skip : test)('readFile - FILE_NOT_DIRECTORY', async () => { @@ -1431,7 +1422,7 @@ suite('Disk File Service', function () { } assert.ok(error); - assert.equal(error!.fileOperationResult, FileOperationResult.FILE_NOT_DIRECTORY); + assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_NOT_DIRECTORY); }); test('readFile - FILE_NOT_FOUND', async () => { @@ -1445,7 +1436,7 @@ suite('Disk File Service', function () { } assert.ok(error); - assert.equal(error!.fileOperationResult, FileOperationResult.FILE_NOT_FOUND); + assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_NOT_FOUND); }); test('readFile - FILE_NOT_MODIFIED_SINCE - default', async () => { @@ -1484,8 +1475,8 @@ suite('Disk File Service', function () { } assert.ok(error); - assert.equal(error!.fileOperationResult, FileOperationResult.FILE_NOT_MODIFIED_SINCE); - assert.equal(fileProvider.totalBytesRead, 0); + assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_NOT_MODIFIED_SINCE); + assert.strictEqual(fileProvider.totalBytesRead, 0); } test('readFile - FILE_NOT_MODIFIED_SINCE does not fire wrongly - https://github.com/microsoft/vscode/issues/72909', async () => { @@ -1546,26 +1537,26 @@ suite('Disk File Service', function () { } assert.ok(error); - assert.equal(error!.fileOperationResult, FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT); + assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT); } - (isWindows ? test.skip /* flaky test */ : test)('readFile - FILE_TOO_LARGE - default', async () => { + test('readFile - FILE_TOO_LARGE - default', async () => { return testFileTooLarge(); }); - (isWindows ? test.skip /* flaky test */ : test)('readFile - FILE_TOO_LARGE - buffered', async () => { + test('readFile - FILE_TOO_LARGE - buffered', async () => { setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose); return testFileTooLarge(); }); - (isWindows ? test.skip /* flaky test */ : test)('readFile - FILE_TOO_LARGE - unbuffered', async () => { + test('readFile - FILE_TOO_LARGE - unbuffered', async () => { setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite); return testFileTooLarge(); }); - (isWindows ? test.skip /* flaky test */ : test)('readFile - FILE_TOO_LARGE - streamed', async () => { + test('readFile - FILE_TOO_LARGE - streamed', async () => { setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadStream); return testFileTooLarge(); @@ -1590,9 +1581,23 @@ suite('Disk File Service', function () { } assert.ok(error); - assert.equal(error!.fileOperationResult, FileOperationResult.FILE_TOO_LARGE); + assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_TOO_LARGE); } + (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('readFile - dangling symbolic link - https://github.com/microsoft/vscode/issues/116049', async () => { + const link = URI.file(join(testDir, 'small.js-link')); + await symlink(join(testDir, 'small.js'), link.fsPath); + + let error: FileOperationError | undefined = undefined; + try { + await service.readFile(link); + } catch (err) { + error = err; + } + + assert.ok(error); + }); + test('createFile', async () => { return assertCreateFile(contents => VSBuffer.fromString(contents)); }); @@ -1612,16 +1617,16 @@ suite('Disk File Service', function () { const contents = 'Hello World'; const resource = URI.file(join(testDir, 'test.txt')); - assert.equal(await service.canCreateFile(resource), true); + assert.strictEqual(await service.canCreateFile(resource), true); const fileStat = await service.createFile(resource, converter(contents)); - assert.equal(fileStat.name, 'test.txt'); - assert.equal(existsSync(fileStat.resource.fsPath), true); - assert.equal(readFileSync(fileStat.resource.fsPath), contents); + assert.strictEqual(fileStat.name, 'test.txt'); + assert.strictEqual(existsSync(fileStat.resource.fsPath), true); + assert.strictEqual(readFileSync(fileStat.resource.fsPath).toString(), contents); assert.ok(event!); - assert.equal(event!.resource.fsPath, resource.fsPath); - assert.equal(event!.operation, FileOperation.CREATE); - assert.equal(event!.target!.resource.fsPath, resource.fsPath); + assert.strictEqual(event!.resource.fsPath, resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.CREATE); + assert.strictEqual(event!.target!.resource.fsPath, resource.fsPath); } test('createFile (does not overwrite by default)', async () => { @@ -1651,16 +1656,16 @@ suite('Disk File Service', function () { writeFileSync(resource.fsPath, ''); // create file - assert.equal(await service.canCreateFile(resource, { overwrite: true }), true); + assert.strictEqual(await service.canCreateFile(resource, { overwrite: true }), true); const fileStat = await service.createFile(resource, VSBuffer.fromString(contents), { overwrite: true }); - assert.equal(fileStat.name, 'test.txt'); - assert.equal(existsSync(fileStat.resource.fsPath), true); - assert.equal(readFileSync(fileStat.resource.fsPath), contents); + assert.strictEqual(fileStat.name, 'test.txt'); + assert.strictEqual(existsSync(fileStat.resource.fsPath), true); + assert.strictEqual(readFileSync(fileStat.resource.fsPath).toString(), contents); assert.ok(event!); - assert.equal(event!.resource.fsPath, resource.fsPath); - assert.equal(event!.operation, FileOperation.CREATE); - assert.equal(event!.target!.resource.fsPath, resource.fsPath); + assert.strictEqual(event!.resource.fsPath, resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.CREATE); + assert.strictEqual(event!.target!.resource.fsPath, resource.fsPath); }); test('writeFile - default', async () => { @@ -1682,13 +1687,13 @@ suite('Disk File Service', function () { async function testWriteFile() { const resource = URI.file(join(testDir, 'small.txt')); - const content = readFileSync(resource.fsPath); - assert.equal(content, 'Small File'); + const content = readFileSync(resource.fsPath).toString(); + assert.strictEqual(content, 'Small File'); const newContent = 'Updates to the small file'; await service.writeFile(resource, VSBuffer.fromString(newContent)); - assert.equal(readFileSync(resource.fsPath), newContent); + assert.strictEqual(readFileSync(resource.fsPath).toString(), newContent); } test('writeFile (large file) - default', async () => { @@ -1714,9 +1719,9 @@ suite('Disk File Service', function () { const newContent = content.toString() + content.toString(); const fileStat = await service.writeFile(resource, VSBuffer.fromString(newContent)); - assert.equal(fileStat.name, 'lorem.txt'); + assert.strictEqual(fileStat.name, 'lorem.txt'); - assert.equal(readFileSync(resource.fsPath), newContent); + assert.strictEqual(readFileSync(resource.fsPath).toString(), newContent); } test('writeFile - buffered - readonly throws', async () => { @@ -1734,8 +1739,8 @@ suite('Disk File Service', function () { async function testWriteFileReadonlyThrows() { const resource = URI.file(join(testDir, 'small.txt')); - const content = readFileSync(resource.fsPath); - assert.equal(content, 'Small File'); + const content = readFileSync(resource.fsPath).toString(); + assert.strictEqual(content, 'Small File'); const newContent = 'Updates to the small file'; @@ -1757,7 +1762,7 @@ suite('Disk File Service', function () { await Promise.all(['0', '00', '000', '0000', '00000'].map(async offset => { const fileStat = await service.writeFile(resource, VSBuffer.fromString(offset + newContent)); - assert.equal(fileStat.name, 'lorem.txt'); + assert.strictEqual(fileStat.name, 'lorem.txt'); })); const fileContent = readFileSync(resource.fsPath).toString(); @@ -1783,13 +1788,13 @@ suite('Disk File Service', function () { async function testWriteFileReadable() { const resource = URI.file(join(testDir, 'small.txt')); - const content = readFileSync(resource.fsPath); - assert.equal(content, 'Small File'); + const content = readFileSync(resource.fsPath).toString(); + assert.strictEqual(content, 'Small File'); const newContent = 'Updates to the small file'; await service.writeFile(resource, toLineByLineReadable(newContent)); - assert.equal(readFileSync(resource.fsPath), newContent); + assert.strictEqual(readFileSync(resource.fsPath).toString(), newContent); } test('writeFile (large file - readable) - default', async () => { @@ -1815,9 +1820,9 @@ suite('Disk File Service', function () { const newContent = content.toString() + content.toString(); const fileStat = await service.writeFile(resource, toLineByLineReadable(newContent)); - assert.equal(fileStat.name, 'lorem.txt'); + assert.strictEqual(fileStat.name, 'lorem.txt'); - assert.equal(readFileSync(resource.fsPath), newContent); + assert.strictEqual(readFileSync(resource.fsPath).toString(), newContent); } test('writeFile (stream) - default', async () => { @@ -1841,10 +1846,10 @@ suite('Disk File Service', function () { const target = URI.file(join(testDir, 'small-copy.txt')); const fileStat = await service.writeFile(target, streamToBufferReadableStream(createReadStream(source.fsPath))); - assert.equal(fileStat.name, 'small-copy.txt'); + assert.strictEqual(fileStat.name, 'small-copy.txt'); const targetContents = readFileSync(target.fsPath).toString(); - assert.equal(readFileSync(source.fsPath).toString(), targetContents); + assert.strictEqual(readFileSync(source.fsPath).toString(), targetContents); } test('writeFile (large file - stream) - default', async () => { @@ -1868,10 +1873,10 @@ suite('Disk File Service', function () { const target = URI.file(join(testDir, 'lorem-copy.txt')); const fileStat = await service.writeFile(target, streamToBufferReadableStream(createReadStream(source.fsPath))); - assert.equal(fileStat.name, 'lorem-copy.txt'); + assert.strictEqual(fileStat.name, 'lorem-copy.txt'); const targetContents = readFileSync(target.fsPath).toString(); - assert.equal(readFileSync(source.fsPath).toString(), targetContents); + assert.strictEqual(readFileSync(source.fsPath).toString(), targetContents); } test('writeFile (file is created including parents)', async () => { @@ -1879,9 +1884,9 @@ suite('Disk File Service', function () { const content = 'File is created including parent'; const fileStat = await service.writeFile(resource, VSBuffer.fromString(content)); - assert.equal(fileStat.name, 'newfile.txt'); + assert.strictEqual(fileStat.name, 'newfile.txt'); - assert.equal(readFileSync(resource.fsPath), content); + assert.strictEqual(readFileSync(resource.fsPath).toString(), content); }); test('writeFile (error when folder is encountered)', async () => { @@ -1902,13 +1907,13 @@ suite('Disk File Service', function () { const stat = await service.resolve(resource); - const content = readFileSync(resource.fsPath); - assert.equal(content, 'Small File'); + const content = readFileSync(resource.fsPath).toString(); + assert.strictEqual(content, 'Small File'); const newContent = 'Updates to the small file'; await service.writeFile(resource, VSBuffer.fromString(newContent), { etag: stat.etag, mtime: stat.mtime }); - assert.equal(readFileSync(resource.fsPath), newContent); + assert.strictEqual(readFileSync(resource.fsPath).toString(), newContent); }); test('writeFile - error when writing to file that has been updated meanwhile', async () => { @@ -1917,7 +1922,7 @@ suite('Disk File Service', function () { const stat = await service.resolve(resource); const content = readFileSync(resource.fsPath).toString(); - assert.equal(content, 'Small File'); + assert.strictEqual(content, 'Small File'); const newContent = 'Updates to the small file'; await service.writeFile(resource, VSBuffer.fromString(newContent), { etag: stat.etag, mtime: stat.mtime }); @@ -1936,7 +1941,7 @@ suite('Disk File Service', function () { assert.ok(error); assert.ok(error instanceof FileOperationError); - assert.equal(error!.fileOperationResult, FileOperationResult.FILE_MODIFIED_SINCE); + assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_MODIFIED_SINCE); }); test('writeFile - no error when writing to file where size is the same', async () => { @@ -1945,7 +1950,7 @@ suite('Disk File Service', function () { const stat = await service.resolve(resource); const content = readFileSync(resource.fsPath).toString(); - assert.equal(content, 'Small File'); + assert.strictEqual(content, 'Small File'); const newContent = content; // same content await service.writeFile(resource, VSBuffer.fromString(newContent), { etag: stat.etag, mtime: stat.mtime }); @@ -2008,73 +2013,72 @@ suite('Disk File Service', function () { const runWatchTests = isLinux; - (runWatchTests ? test : test.skip)('watch - file', done => { + (runWatchTests ? test : test.skip)('watch - file', async () => { const toWatch = URI.file(join(testDir, 'index-watch1.html')); writeFileSync(toWatch.fsPath, 'Init'); - assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done); - + const promise = assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]]); setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes'), 50); + await promise; }); - (runWatchTests && !isWindows /* symbolic links not reliable on windows */ ? test : test.skip)('watch - file symbolic link', async done => { + (runWatchTests && !isWindows /* windows: cannot create file symbolic link without elevated context */ ? test : test.skip)('watch - file symbolic link', async () => { const toWatch = URI.file(join(testDir, 'lorem.txt-linked')); await symlink(join(testDir, 'lorem.txt'), toWatch.fsPath); - assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done); - + const promise = assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]]); setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes'), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - file - multiple writes', done => { + (runWatchTests ? test : test.skip)('watch - file - multiple writes', async () => { const toWatch = URI.file(join(testDir, 'index-watch1.html')); writeFileSync(toWatch.fsPath, 'Init'); - assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done); - + const promise = assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]]); setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes 1'), 0); setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes 2'), 10); setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes 3'), 20); + await promise; }); - (runWatchTests ? test : test.skip)('watch - file - delete file', done => { + (runWatchTests ? test : test.skip)('watch - file - delete file', async () => { const toWatch = URI.file(join(testDir, 'index-watch1.html')); writeFileSync(toWatch.fsPath, 'Init'); - assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]], done); - + const promise = assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]]); setTimeout(() => unlinkSync(toWatch.fsPath), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - file - rename file', done => { + (runWatchTests ? test : test.skip)('watch - file - rename file', async () => { const toWatch = URI.file(join(testDir, 'index-watch1.html')); const toWatchRenamed = URI.file(join(testDir, 'index-watch1-renamed.html')); writeFileSync(toWatch.fsPath, 'Init'); - assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]], done); - + const promise = assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]]); setTimeout(() => renameSync(toWatch.fsPath, toWatchRenamed.fsPath), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - file - rename file (different case)', done => { + (runWatchTests ? test : test.skip)('watch - file - rename file (different case)', async () => { const toWatch = URI.file(join(testDir, 'index-watch1.html')); const toWatchRenamed = URI.file(join(testDir, 'INDEX-watch1.html')); writeFileSync(toWatch.fsPath, 'Init'); - if (isLinux) { - assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]], done); - } else { - assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done); // case insensitive file system treat this as change - } + const promise = isLinux + ? assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]]) + : assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]]); // case insensitive file system treat this as change setTimeout(() => renameSync(toWatch.fsPath, toWatchRenamed.fsPath), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - file (atomic save)', function (done) { + (runWatchTests ? test : test.skip)('watch - file (atomic save)', async () => { const toWatch = URI.file(join(testDir, 'index-watch2.html')); writeFileSync(toWatch.fsPath, 'Init'); - assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done); + const promise = assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]]); setTimeout(() => { // Simulate atomic save by deleting the file, creating it under different name @@ -2084,79 +2088,81 @@ suite('Disk File Service', function () { writeFileSync(renamed, 'Changes'); renameSync(renamed, toWatch.fsPath); }, 50); + + await promise; }); - (runWatchTests ? test : test.skip)('watch - folder (non recursive) - change file', done => { + (runWatchTests ? test : test.skip)('watch - folder (non recursive) - change file', async () => { const watchDir = URI.file(join(testDir, 'watch3')); mkdirSync(watchDir.fsPath); const file = URI.file(join(watchDir.fsPath, 'index.html')); writeFileSync(file.fsPath, 'Init'); - assertWatch(watchDir, [[FileChangeType.UPDATED, file]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.UPDATED, file]]); setTimeout(() => writeFileSync(file.fsPath, 'Changes'), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - folder (non recursive) - add file', done => { + (runWatchTests ? test : test.skip)('watch - folder (non recursive) - add file', async () => { const watchDir = URI.file(join(testDir, 'watch4')); mkdirSync(watchDir.fsPath); const file = URI.file(join(watchDir.fsPath, 'index.html')); - assertWatch(watchDir, [[FileChangeType.ADDED, file]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.ADDED, file]]); setTimeout(() => writeFileSync(file.fsPath, 'Changes'), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - folder (non recursive) - delete file', done => { + (runWatchTests ? test : test.skip)('watch - folder (non recursive) - delete file', async () => { const watchDir = URI.file(join(testDir, 'watch5')); mkdirSync(watchDir.fsPath); const file = URI.file(join(watchDir.fsPath, 'index.html')); writeFileSync(file.fsPath, 'Init'); - assertWatch(watchDir, [[FileChangeType.DELETED, file]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.DELETED, file]]); setTimeout(() => unlinkSync(file.fsPath), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - folder (non recursive) - add folder', done => { + (runWatchTests ? test : test.skip)('watch - folder (non recursive) - add folder', async () => { const watchDir = URI.file(join(testDir, 'watch6')); mkdirSync(watchDir.fsPath); const folder = URI.file(join(watchDir.fsPath, 'folder')); - assertWatch(watchDir, [[FileChangeType.ADDED, folder]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.ADDED, folder]]); setTimeout(() => mkdirSync(folder.fsPath), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - folder (non recursive) - delete folder', done => { + (runWatchTests ? test : test.skip)('watch - folder (non recursive) - delete folder', async () => { const watchDir = URI.file(join(testDir, 'watch7')); mkdirSync(watchDir.fsPath); const folder = URI.file(join(watchDir.fsPath, 'folder')); mkdirSync(folder.fsPath); - assertWatch(watchDir, [[FileChangeType.DELETED, folder]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.DELETED, folder]]); setTimeout(() => rimrafSync(folder.fsPath), 50); + await promise; }); - (runWatchTests && !isWindows /* symbolic links not reliable on windows */ ? test : test.skip)('watch - folder (non recursive) - symbolic link - change file', async done => { + (runWatchTests ? test : test.skip)('watch - folder (non recursive) - symbolic link - change file', async () => { const watchDir = URI.file(join(testDir, 'deep-link')); - await symlink(join(testDir, 'deep'), watchDir.fsPath); + await symlink(join(testDir, 'deep'), watchDir.fsPath, 'junction'); const file = URI.file(join(watchDir.fsPath, 'index.html')); writeFileSync(file.fsPath, 'Init'); - assertWatch(watchDir, [[FileChangeType.UPDATED, file]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.UPDATED, file]]); setTimeout(() => writeFileSync(file.fsPath, 'Changes'), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - folder (non recursive) - rename file', done => { + (runWatchTests ? test : test.skip)('watch - folder (non recursive) - rename file', async () => { const watchDir = URI.file(join(testDir, 'watch8')); mkdirSync(watchDir.fsPath); @@ -2165,12 +2171,12 @@ suite('Disk File Service', function () { const fileRenamed = URI.file(join(watchDir.fsPath, 'index-renamed.html')); - assertWatch(watchDir, [[FileChangeType.DELETED, file], [FileChangeType.ADDED, fileRenamed]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.DELETED, file], [FileChangeType.ADDED, fileRenamed]]); setTimeout(() => renameSync(file.fsPath, fileRenamed.fsPath), 50); + await promise; }); - (runWatchTests && isLinux /* this test requires a case sensitive file system */ ? test : test.skip)('watch - folder (non recursive) - rename file (different case)', done => { + (runWatchTests && isLinux /* this test requires a case sensitive file system */ ? test : test.skip)('watch - folder (non recursive) - rename file (different case)', async () => { const watchDir = URI.file(join(testDir, 'watch8')); mkdirSync(watchDir.fsPath); @@ -2179,46 +2185,48 @@ suite('Disk File Service', function () { const fileRenamed = URI.file(join(watchDir.fsPath, 'INDEX.html')); - assertWatch(watchDir, [[FileChangeType.DELETED, file], [FileChangeType.ADDED, fileRenamed]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.DELETED, file], [FileChangeType.ADDED, fileRenamed]]); setTimeout(() => renameSync(file.fsPath, fileRenamed.fsPath), 50); + await promise; }); - function assertWatch(toWatch: URI, expected: [FileChangeType, URI][], done: MochaDone): void { - const watcherDisposable = service.watch(toWatch); + function assertWatch(toWatch: URI, expected: [FileChangeType, URI][]): Promise { + return new Promise((resolve, reject) => { + const watcherDisposable = service.watch(toWatch); - function toString(type: FileChangeType): string { - switch (type) { - case FileChangeType.ADDED: return 'added'; - case FileChangeType.DELETED: return 'deleted'; - case FileChangeType.UPDATED: return 'updated'; - } - } - - function printEvents(event: FileChangesEvent): string { - return event.changes.map(change => `Change: type ${toString(change.type)} path ${change.resource.toString()}`).join('\n'); - } - - const listenerDisposable = service.onDidFilesChange(event => { - watcherDisposable.dispose(); - listenerDisposable.dispose(); - - try { - assert.equal(event.changes.length, expected.length, `Expected ${expected.length} events, but got ${event.changes.length}. Details (${printEvents(event)})`); - - if (expected.length === 1) { - assert.equal(event.changes[0].type, expected[0][0], `Expected ${toString(expected[0][0])} but got ${toString(event.changes[0].type)}. Details (${printEvents(event)})`); - assert.equal(event.changes[0].resource.fsPath, expected[0][1].fsPath); - } else { - for (const expect of expected) { - assert.equal(hasChange(event.changes, expect[0], expect[1]), true, `Unable to find ${toString(expect[0])} for ${expect[1].fsPath}. Details (${printEvents(event)})`); - } + function toString(type: FileChangeType): string { + switch (type) { + case FileChangeType.ADDED: return 'added'; + case FileChangeType.DELETED: return 'deleted'; + case FileChangeType.UPDATED: return 'updated'; } - - done(); - } catch (error) { - done(error); } + + function printEvents(event: FileChangesEvent): string { + return event.changes.map(change => `Change: type ${toString(change.type)} path ${change.resource.toString()}`).join('\n'); + } + + const listenerDisposable = service.onDidFilesChange(event => { + watcherDisposable.dispose(); + listenerDisposable.dispose(); + + try { + assert.strictEqual(event.changes.length, expected.length, `Expected ${expected.length} events, but got ${event.changes.length}. Details (${printEvents(event)})`); + + if (expected.length === 1) { + assert.strictEqual(event.changes[0].type, expected[0][0], `Expected ${toString(expected[0][0])} but got ${toString(event.changes[0].type)}. Details (${printEvents(event)})`); + assert.strictEqual(event.changes[0].resource.fsPath, expected[0][1].fsPath); + } else { + for (const expect of expected) { + assert.strictEqual(hasChange(event.changes, expect[0], expect[1]), true, `Unable to find ${toString(expect[0])} for ${expect[1].fsPath}. Details (${printEvents(event)})`); + } + } + + resolve(); + } catch (error) { + reject(error); + } + }); }); } @@ -2234,7 +2242,7 @@ suite('Disk File Service', function () { let fd = await fileProvider.open(resource, { create: false }); for (let i = 0; i < 3; i++) { await fileProvider.read(fd, 0, buffer.buffer, 0, 26); - assert.equal(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); + assert.strictEqual(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); } await fileProvider.close(fd); @@ -2245,31 +2253,31 @@ suite('Disk File Service', function () { let posInFile = 0; await fileProvider.read(fd, posInFile, buffer.buffer, 0, 26); - assert.equal(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); + assert.strictEqual(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); posInFile += 26; await fileProvider.read(fd, posInFile, buffer.buffer, 0, 1); - assert.equal(buffer.slice(0, 1).toString(), ','); + assert.strictEqual(buffer.slice(0, 1).toString(), ','); posInFile += 1; await fileProvider.read(fd, posInFile, buffer.buffer, 0, 12); - assert.equal(buffer.slice(0, 12).toString(), ' consectetur'); + assert.strictEqual(buffer.slice(0, 12).toString(), ' consectetur'); posInFile += 12; await fileProvider.read(fd, 98 /* no longer in sequence of posInFile */, buffer.buffer, 0, 9); - assert.equal(buffer.slice(0, 9).toString(), 'fermentum'); + assert.strictEqual(buffer.slice(0, 9).toString(), 'fermentum'); await fileProvider.read(fd, 27, buffer.buffer, 0, 12); - assert.equal(buffer.slice(0, 12).toString(), ' consectetur'); + assert.strictEqual(buffer.slice(0, 12).toString(), ' consectetur'); await fileProvider.read(fd, 26, buffer.buffer, 0, 1); - assert.equal(buffer.slice(0, 1).toString(), ','); + assert.strictEqual(buffer.slice(0, 1).toString(), ','); await fileProvider.read(fd, 0, buffer.buffer, 0, 26); - assert.equal(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); + assert.strictEqual(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); await fileProvider.read(fd, posInFile /* back in sequence */, buffer.buffer, 0, 11); - assert.equal(buffer.slice(0, 11).toString(), ' adipiscing'); + assert.strictEqual(buffer.slice(0, 11).toString(), ' adipiscing'); await fileProvider.close(fd); }); @@ -2289,7 +2297,7 @@ suite('Disk File Service', function () { posInFileWrite += initialContents.byteLength; await fileProvider.read(fdRead, posInFileRead, buffer.buffer, 0, 26); - assert.equal(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); + assert.strictEqual(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); posInFileRead += 26; const contents = VSBuffer.fromString('Hello World'); @@ -2298,19 +2306,19 @@ suite('Disk File Service', function () { posInFileWrite += contents.byteLength; await fileProvider.read(fdRead, posInFileRead, buffer.buffer, 0, contents.byteLength); - assert.equal(buffer.slice(0, contents.byteLength).toString(), 'Hello World'); + assert.strictEqual(buffer.slice(0, contents.byteLength).toString(), 'Hello World'); posInFileRead += contents.byteLength; await fileProvider.write(fdWrite, 6, contents.buffer, 0, contents.byteLength); await fileProvider.read(fdRead, 0, buffer.buffer, 0, 11); - assert.equal(buffer.slice(0, 11).toString(), 'Lorem Hello'); + assert.strictEqual(buffer.slice(0, 11).toString(), 'Lorem Hello'); await fileProvider.write(fdWrite, posInFileWrite, contents.buffer, 0, contents.byteLength); posInFileWrite += contents.byteLength; await fileProvider.read(fdRead, posInFileWrite - contents.byteLength, buffer.buffer, 0, contents.byteLength); - assert.equal(buffer.slice(0, contents.byteLength).toString(), 'Hello World'); + assert.strictEqual(buffer.slice(0, contents.byteLength).toString(), 'Hello World'); await fileProvider.close(fdWrite); await fileProvider.close(fdRead); diff --git a/src/vs/platform/files/test/electron-browser/normalizer.test.ts b/src/vs/platform/files/test/electron-browser/normalizer.test.ts index 98aa1162..df672c5f 100644 --- a/src/vs/platform/files/test/electron-browser/normalizer.test.ts +++ b/src/vs/platform/files/test/electron-browser/normalizer.test.ts @@ -64,7 +64,7 @@ suite('Normalizer', () => { watch.onDidFilesChange(e => { assert.ok(e); - assert.equal(e.changes.length, 3); + assert.strictEqual(e.changes.length, 3); assert.ok(e.contains(added, FileChangeType.ADDED)); assert.ok(e.contains(updated, FileChangeType.UPDATED)); assert.ok(e.contains(deleted, FileChangeType.DELETED)); @@ -103,7 +103,7 @@ suite('Normalizer', () => { watch.onDidFilesChange(e => { assert.ok(e); - assert.equal(e.changes.length, 5); + assert.strictEqual(e.changes.length, 5); assert.ok(e.contains(deletedFolderA, FileChangeType.DELETED)); assert.ok(e.contains(deletedFolderB, FileChangeType.DELETED)); @@ -133,7 +133,7 @@ suite('Normalizer', () => { watch.onDidFilesChange(e => { assert.ok(e); - assert.equal(e.changes.length, 1); + assert.strictEqual(e.changes.length, 1); assert.ok(e.contains(unrelated, FileChangeType.UPDATED)); @@ -158,7 +158,7 @@ suite('Normalizer', () => { watch.onDidFilesChange(e => { assert.ok(e); - assert.equal(e.changes.length, 2); + assert.strictEqual(e.changes.length, 2); assert.ok(e.contains(deleted, FileChangeType.UPDATED)); assert.ok(e.contains(unrelated, FileChangeType.UPDATED)); @@ -184,7 +184,7 @@ suite('Normalizer', () => { watch.onDidFilesChange(e => { assert.ok(e); - assert.equal(e.changes.length, 2); + assert.strictEqual(e.changes.length, 2); assert.ok(e.contains(created, FileChangeType.ADDED)); assert.ok(!e.contains(created, FileChangeType.UPDATED)); @@ -213,7 +213,7 @@ suite('Normalizer', () => { watch.onDidFilesChange(e => { assert.ok(e); - assert.equal(e.changes.length, 2); + assert.strictEqual(e.changes.length, 2); assert.ok(e.contains(deleted, FileChangeType.DELETED)); assert.ok(!e.contains(updated, FileChangeType.UPDATED)); diff --git a/src/vs/platform/instantiation/common/instantiationService.ts b/src/vs/platform/instantiation/common/instantiationService.ts index 0fd23fd5..00f0cb9d 100644 --- a/src/vs/platform/instantiation/common/instantiationService.ts +++ b/src/vs/platform/instantiation/common/instantiationService.ts @@ -132,15 +132,31 @@ export class InstantiationService implements IInstantiationService { private _getOrCreateServiceInstance(id: ServiceIdentifier, _trace: Trace): T { let thing = this._getServiceInstanceOrDescriptor(id); if (thing instanceof SyncDescriptor) { - return this._createAndCacheServiceInstance(id, thing, _trace.branch(id, true)); + return this._safeCreateAndCacheServiceInstance(id, thing, _trace.branch(id, true)); } else { _trace.branch(id, false); return thing; } } + private readonly _activeInstantiations = new Set>(); + + + private _safeCreateAndCacheServiceInstance(id: ServiceIdentifier, desc: SyncDescriptor, _trace: Trace): T { + if (this._activeInstantiations.has(id)) { + throw new Error(`illegal state - RECURSIVELY instantiating service '${id}'`); + } + this._activeInstantiations.add(id); + try { + return this._createAndCacheServiceInstance(id, desc, _trace); + } finally { + this._activeInstantiations.delete(id); + } + } + private _createAndCacheServiceInstance(id: ServiceIdentifier, desc: SyncDescriptor, _trace: Trace): T { - type Triple = { id: ServiceIdentifier, desc: SyncDescriptor, _trace: Trace }; + + type Triple = { id: ServiceIdentifier, desc: SyncDescriptor, _trace: Trace; }; const graph = new Graph(data => data.id.toString()); let cycleCount = 0; @@ -195,7 +211,6 @@ export class InstantiationService implements IInstantiationService { graph.removeNode(data); } } - return this._getServiceInstanceOrDescriptor(id); } diff --git a/src/vs/platform/ipc/electron-browser/sharedProcessService.ts b/src/vs/platform/ipc/electron-browser/sharedProcessService.ts index 59fb5a6b..60299f96 100644 --- a/src/vs/platform/ipc/electron-browser/sharedProcessService.ts +++ b/src/vs/platform/ipc/electron-browser/sharedProcessService.ts @@ -4,7 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { Event } from 'vs/base/common/event'; +import { IpcRendererEvent } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes'; +import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { Client as MessagePortClient } from 'vs/base/parts/ipc/common/ipc.mp'; +import { IChannel, IServerChannel, getDelayedChannel } from 'vs/base/parts/ipc/common/ipc'; +import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; +import { generateUuid } from 'vs/base/common/uuid'; +import { ILogService } from 'vs/platform/log/common/log'; +import { Disposable } from 'vs/base/common/lifecycle'; export const ISharedProcessService = createDecorator('sharedProcessService'); @@ -14,7 +22,47 @@ export interface ISharedProcessService { getChannel(channelName: string): IChannel; registerChannel(channelName: string, channel: IServerChannel): void; - - whenSharedProcessReady(): Promise; - toggleSharedProcessWindow(): Promise; +} + +export class SharedProcessService extends Disposable implements ISharedProcessService { + + declare readonly _serviceBrand: undefined; + + private readonly withSharedProcessConnection: Promise; + + constructor( + @INativeHostService private readonly nativeHostService: INativeHostService, + @ILogService private readonly logService: ILogService + ) { + super(); + + this.withSharedProcessConnection = this.connect(); + } + + private async connect(): Promise { + this.logService.trace('Renderer->SharedProcess#connect'); + + // Ask to create message channel inside the window + // and send over a UUID to correlate the response + const nonce = generateUuid(); + ipcRenderer.send('vscode:createSharedProcessMessageChannel', nonce); + + // Wait until the main side has returned the `MessagePort` + // We need to filter by the `nonce` to ensure we listen + // to the right response. + const onMessageChannelResult = Event.fromNodeEventEmitter<{ nonce: string, port: MessagePort }>(ipcRenderer, 'vscode:createSharedProcessMessageChannelResult', (e: IpcRendererEvent, nonce: string) => ({ nonce, port: e.ports[0] })); + const { port } = await Event.toPromise(Event.once(Event.filter(onMessageChannelResult, e => e.nonce === nonce))); + + this.logService.trace('Renderer->SharedProcess#connect: connection established'); + + return this._register(new MessagePortClient(port, `window:${this.nativeHostService.windowId}`)); + } + + getChannel(channelName: string): IChannel { + return getDelayedChannel(this.withSharedProcessConnection.then(connection => connection.getChannel(channelName))); + } + + registerChannel(channelName: string, channel: IServerChannel): void { + this.withSharedProcessConnection.then(connection => connection.registerChannel(channelName, channel)); + } } diff --git a/src/vs/platform/ipc/electron-main/sharedProcessMainService.ts b/src/vs/platform/ipc/electron-main/sharedProcessMainService.ts deleted file mode 100644 index 97decd56..00000000 --- a/src/vs/platform/ipc/electron-main/sharedProcessMainService.ts +++ /dev/null @@ -1,36 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; - -export const ISharedProcessMainService = createDecorator('sharedProcessMainService'); - -export interface ISharedProcessMainService { - - readonly _serviceBrand: undefined; - - whenSharedProcessReady(): Promise; - toggleSharedProcessWindow(): Promise; -} - -export interface ISharedProcess { - whenReady(): Promise; - toggle(): void; -} - -export class SharedProcessMainService implements ISharedProcessMainService { - - declare readonly _serviceBrand: undefined; - - constructor(private sharedProcess: ISharedProcess) { } - - whenSharedProcessReady(): Promise { - return this.sharedProcess.whenReady(); - } - - async toggleSharedProcessWindow(): Promise { - return this.sharedProcess.toggle(); - } -} diff --git a/src/vs/platform/ipc/electron-sandbox/mainProcessService.ts b/src/vs/platform/ipc/electron-sandbox/mainProcessService.ts index b2c78a33..2f326523 100644 --- a/src/vs/platform/ipc/electron-sandbox/mainProcessService.ts +++ b/src/vs/platform/ipc/electron-sandbox/mainProcessService.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { Client } from 'vs/base/parts/ipc/electron-sandbox/ipc.electron-sandbox'; +import { IChannel, IServerChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; +import { Client as IPCElectronClient } from 'vs/base/parts/ipc/electron-sandbox/ipc.electron'; import { Disposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { Server as MessagePortServer } from 'vs/base/parts/ipc/electron-sandbox/ipc.mp'; export const IMainProcessService = createDecorator('mainProcessService'); @@ -19,18 +20,21 @@ export interface IMainProcessService { registerChannel(channelName: string, channel: IServerChannel): void; } -export class MainProcessService extends Disposable implements IMainProcessService { +/** + * An implementation of `IMainProcessService` that leverages Electron's IPC. + */ +export class ElectronIPCMainProcessService extends Disposable implements IMainProcessService { declare readonly _serviceBrand: undefined; - private mainProcessConnection: Client; + private mainProcessConnection: IPCElectronClient; constructor( windowId: number ) { super(); - this.mainProcessConnection = this._register(new Client(`window:${windowId}`)); + this.mainProcessConnection = this._register(new IPCElectronClient(`window:${windowId}`)); } getChannel(channelName: string): IChannel { @@ -41,3 +45,24 @@ export class MainProcessService extends Disposable implements IMainProcessServic this.mainProcessConnection.registerChannel(channelName, channel); } } + +/** + * An implementation of `IMainProcessService` that leverages MessagePorts. + */ +export class MessagePortMainProcessService implements IMainProcessService { + + declare readonly _serviceBrand: undefined; + + constructor( + private server: MessagePortServer, + private router: StaticRouter + ) { } + + getChannel(channelName: string): IChannel { + return this.server.getChannel(channelName, this.router); + } + + registerChannel(channelName: string, channel: IServerChannel): void { + this.server.registerChannel(channelName, channel); + } +} diff --git a/src/vs/platform/issue/common/issue.ts b/src/vs/platform/issue/common/issue.ts index e981a34d..fc36369c 100644 --- a/src/vs/platform/issue/common/issue.ts +++ b/src/vs/platform/issue/common/issue.ts @@ -56,6 +56,7 @@ export interface IssueReporterData extends WindowData { issueType?: IssueType; extensionId?: string; experiments?: string; + githubAccessToken: string; readonly issueTitle?: string; readonly issueBody?: string; } @@ -72,7 +73,6 @@ export interface IssueReporterFeatures { export interface ProcessExplorerStyles extends WindowStyles { hoverBackground?: string; hoverForeground?: string; - highlightForeground?: string; } export interface ProcessExplorerData extends WindowData { diff --git a/src/vs/platform/issue/electron-main/issueMainService.ts b/src/vs/platform/issue/electron-main/issueMainService.ts index da98e2f9..994f4559 100644 --- a/src/vs/platform/issue/electron-main/issueMainService.ts +++ b/src/vs/platform/issue/electron-main/issueMainService.ts @@ -17,7 +17,7 @@ import { isMacintosh, IProcessEnvironment } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; import { IWindowState } from 'vs/platform/windows/electron-main/windows'; import { listProcesses } from 'vs/base/node/ps'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { zoomLevelToZoomFactor } from 'vs/platform/windows/common/windows'; import { FileAccess } from 'vs/base/common/network'; @@ -185,120 +185,118 @@ export class IssueMainService implements ICommonIssueService { } } - openReporter(data: IssueReporterData): Promise { - return new Promise(_ => { - if (!this._issueWindow) { - this._issueParentWindow = BrowserWindow.getFocusedWindow(); - if (this._issueParentWindow) { - const position = this.getWindowPosition(this._issueParentWindow, 700, 800); + async openReporter(data: IssueReporterData): Promise { + if (!this._issueWindow) { + this._issueParentWindow = BrowserWindow.getFocusedWindow(); + if (this._issueParentWindow) { + const position = this.getWindowPosition(this._issueParentWindow, 700, 800); - this._issueWindow = new BrowserWindow({ - fullscreen: false, - width: position.width, - height: position.height, - minWidth: 300, - minHeight: 200, - x: position.x, - y: position.y, - title: localize('issueReporter', "Issue Reporter"), - backgroundColor: data.styles.backgroundColor || DEFAULT_BACKGROUND_COLOR, - webPreferences: { - preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, - enableWebSQL: false, - enableRemoteModule: false, - spellcheck: false, - nativeWindowOpen: true, - zoomFactor: zoomLevelToZoomFactor(data.zoomLevel), - sandbox: true, - contextIsolation: true - } - }); + this._issueWindow = new BrowserWindow({ + fullscreen: false, + width: position.width, + height: position.height, + minWidth: 300, + minHeight: 200, + x: position.x, + y: position.y, + title: localize('issueReporter', "Issue Reporter"), + backgroundColor: data.styles.backgroundColor || DEFAULT_BACKGROUND_COLOR, + webPreferences: { + preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, + v8CacheOptions: 'bypassHeatCheck', + enableWebSQL: false, + enableRemoteModule: false, + spellcheck: false, + nativeWindowOpen: true, + zoomFactor: zoomLevelToZoomFactor(data.zoomLevel), + sandbox: true, + contextIsolation: true + } + }); - this._issueWindow.setMenuBarVisibility(false); // workaround for now, until a menu is implemented + this._issueWindow.setMenuBarVisibility(false); // workaround for now, until a menu is implemented - // Modified when testing UI - const features: IssueReporterFeatures = {}; + // Modified when testing UI + const features: IssueReporterFeatures = {}; - this.logService.trace('issueService#openReporter: opening issue reporter'); - this._issueWindow.loadURL(this.getIssueReporterPath(data, features)); + this.logService.trace('issueService#openReporter: opening issue reporter'); + this._issueWindow.loadURL(this.getIssueReporterPath(data, features)); - this._issueWindow.on('close', () => this._issueWindow = null); + this._issueWindow.on('close', () => this._issueWindow = null); - this._issueParentWindow.on('closed', () => { - if (this._issueWindow) { - this._issueWindow.close(); - this._issueWindow = null; - } - }); - } + this._issueParentWindow.on('closed', () => { + if (this._issueWindow) { + this._issueWindow.close(); + this._issueWindow = null; + } + }); } + } - if (this._issueWindow) { - this._issueWindow.focus(); - } - }); + if (this._issueWindow) { + this._issueWindow.focus(); + } } - openProcessExplorer(data: ProcessExplorerData): Promise { - return new Promise(_ => { - // Create as singleton - if (!this._processExplorerWindow) { - this._processExplorerParentWindow = BrowserWindow.getFocusedWindow(); - if (this._processExplorerParentWindow) { - const position = this.getWindowPosition(this._processExplorerParentWindow, 800, 500); - this._processExplorerWindow = new BrowserWindow({ - skipTaskbar: true, - resizable: true, - fullscreen: false, - width: position.width, - height: position.height, - minWidth: 300, - minHeight: 200, - x: position.x, - y: position.y, - backgroundColor: data.styles.backgroundColor, - title: localize('processExplorer', "Process Explorer"), - webPreferences: { - preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, - enableWebSQL: false, - enableRemoteModule: false, - spellcheck: false, - nativeWindowOpen: true, - zoomFactor: zoomLevelToZoomFactor(data.zoomLevel), - sandbox: true, - contextIsolation: true - } - }); + async openProcessExplorer(data: ProcessExplorerData): Promise { + // Create as singleton + if (!this._processExplorerWindow) { + this._processExplorerParentWindow = BrowserWindow.getFocusedWindow(); + if (this._processExplorerParentWindow) { + const position = this.getWindowPosition(this._processExplorerParentWindow, 800, 500); + this._processExplorerWindow = new BrowserWindow({ + skipTaskbar: true, + resizable: true, + fullscreen: false, + width: position.width, + height: position.height, + minWidth: 300, + minHeight: 200, + x: position.x, + y: position.y, + backgroundColor: data.styles.backgroundColor, + title: localize('processExplorer', "Process Explorer"), + webPreferences: { + preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, + v8CacheOptions: 'bypassHeatCheck', + enableWebSQL: false, + enableRemoteModule: false, + spellcheck: false, + nativeWindowOpen: true, + zoomFactor: zoomLevelToZoomFactor(data.zoomLevel), + sandbox: true, + contextIsolation: true + } + }); - this._processExplorerWindow.setMenuBarVisibility(false); + this._processExplorerWindow.setMenuBarVisibility(false); - const windowConfiguration = { - appRoot: this.environmentService.appRoot, - windowId: this._processExplorerWindow.id, - userEnv: this.userEnv, - machineId: this.machineId, - data - }; + const windowConfiguration = { + appRoot: this.environmentService.appRoot, + windowId: this._processExplorerWindow.id, + userEnv: this.userEnv, + machineId: this.machineId, + data + }; - this._processExplorerWindow.loadURL( - toWindowUrl('vs/code/electron-sandbox/processExplorer/processExplorer.html', windowConfiguration)); + this._processExplorerWindow.loadURL( + toWindowUrl('vs/code/electron-sandbox/processExplorer/processExplorer.html', windowConfiguration)); - this._processExplorerWindow.on('close', () => this._processExplorerWindow = null); + this._processExplorerWindow.on('close', () => this._processExplorerWindow = null); - this._processExplorerParentWindow.on('close', () => { - if (this._processExplorerWindow) { - this._processExplorerWindow.close(); - this._processExplorerWindow = null; - } - }); - } + this._processExplorerParentWindow.on('close', () => { + if (this._processExplorerWindow) { + this._processExplorerWindow.close(); + this._processExplorerWindow = null; + } + }); } + } - // Focus - if (this._processExplorerWindow) { - this._processExplorerWindow.focus(); - } - }); + // Focus + if (this._processExplorerWindow) { + this._processExplorerWindow.focus(); + } } public async getSystemStatus(): Promise { @@ -414,7 +412,7 @@ export class IssueMainService implements ICommonIssueService { }, product: { nameShort: product.nameShort, - version: product.version, + version: !!product.darwinUniversalAssetId ? `${product.version} (Universal)` : product.version, commit: product.commit, date: product.date, reportIssueUrl: product.reportIssueUrl @@ -436,7 +434,7 @@ function toWindowUrl(modulePathToHtml: string, windowConfiguration: T): strin } return FileAccess - ._asCodeFileUri(modulePathToHtml, require) + .asBrowserUri(modulePathToHtml, require, true) .with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` }) .toString(true); } diff --git a/src/vs/platform/keybinding/common/keybindingResolver.ts b/src/vs/platform/keybinding/common/keybindingResolver.ts index 3582ce6d..ca31e62e 100644 --- a/src/vs/platform/keybinding/common/keybindingResolver.ts +++ b/src/vs/platform/keybinding/common/keybindingResolver.ts @@ -3,9 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { isNonEmptyArray } from 'vs/base/common/arrays'; -import { MenuRegistry } from 'vs/platform/actions/common/actions'; -import { CommandsRegistry, ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; import { IContext, ContextKeyExpression, ContextKeyExprType } from 'vs/platform/contextkey/common/contextkey'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; @@ -340,39 +337,6 @@ export class KeybindingResolver { } return rules.evaluate(context); } - - public static getAllUnboundCommands(boundCommands: Map): string[] { - const unboundCommands: string[] = []; - const seenMap: Map = new Map(); - const addCommand = (id: string, includeCommandWithArgs: boolean) => { - if (seenMap.has(id)) { - return; - } - seenMap.set(id, true); - if (id[0] === '_' || id.indexOf('vscode.') === 0) { // private command - return; - } - if (boundCommands.get(id) === true) { - return; - } - if (!includeCommandWithArgs) { - const command = CommandsRegistry.getCommand(id); - if (command && typeof command.description === 'object' - && isNonEmptyArray((command.description).args)) { // command with args - return; - } - } - unboundCommands.push(id); - }; - for (const id of MenuRegistry.getCommands().keys()) { - addCommand(id, true); - } - for (const id of CommandsRegistry.getCommands().keys()) { - addCommand(id, false); - } - - return unboundCommands; - } } function printWhenExplanation(when: ContextKeyExpression | undefined): string { diff --git a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts index cbbb5434..d627e662 100644 --- a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts +++ b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts @@ -211,13 +211,13 @@ suite('AbstractKeybindingService', () => { // send Ctrl/Cmd + K let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, []); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, [ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, [ `(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...` ]); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -225,13 +225,13 @@ suite('AbstractKeybindingService', () => { // send backspace shouldPreventDefault = kbService.testDispatch(KeyCode.Backspace); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, []); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, [ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, [ `The key combination (${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}, ${toUsLabel(KeyCode.Backspace)}) is not a command.` ]); - assert.deepEqual(statusMessageCallsDisposed, [ + assert.deepStrictEqual(statusMessageCallsDisposed, [ `(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...` ]); executeCommandCalls = []; @@ -241,14 +241,14 @@ suite('AbstractKeybindingService', () => { // send backspace shouldPreventDefault = kbService.testDispatch(KeyCode.Backspace); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, [{ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'simpleCommand', args: [null] }]); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -273,11 +273,11 @@ suite('AbstractKeybindingService', () => { function assertIsIgnored(keybinding: number): void { let shouldPreventDefault = kbService.testDispatch(keybinding); - assert.equal(shouldPreventDefault, false); - assert.deepEqual(executeCommandCalls, []); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.strictEqual(shouldPreventDefault, false); + assert.deepStrictEqual(executeCommandCalls, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -310,14 +310,14 @@ suite('AbstractKeybindingService', () => { key1: true }); let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, [{ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'simpleCommand', args: [null] }]); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -326,13 +326,13 @@ suite('AbstractKeybindingService', () => { // send Ctrl/Cmd + K currentContextValue = createContext({}); shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, []); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, [ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, [ `(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...` ]); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -341,14 +341,14 @@ suite('AbstractKeybindingService', () => { // send Ctrl/Cmd + X currentContextValue = createContext({}); shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_X); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, [{ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'chordCommand', args: [null] }]); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, [ + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, [ `(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...` ]); executeCommandCalls = []; @@ -370,14 +370,14 @@ suite('AbstractKeybindingService', () => { // send Ctrl/Cmd + K currentContextValue = createContext({}); let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, [{ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'simpleCommand', args: [null] }]); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -388,14 +388,14 @@ suite('AbstractKeybindingService', () => { key1: true }); shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, [{ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'simpleCommand', args: [null] }]); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -406,11 +406,11 @@ suite('AbstractKeybindingService', () => { key1: true }); shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_X); - assert.equal(shouldPreventDefault, false); - assert.deepEqual(executeCommandCalls, []); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.strictEqual(shouldPreventDefault, false); + assert.deepStrictEqual(executeCommandCalls, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -428,14 +428,14 @@ suite('AbstractKeybindingService', () => { // send Ctrl/Cmd + K currentContextValue = createContext({}); let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); - assert.equal(shouldPreventDefault, false); - assert.deepEqual(executeCommandCalls, [{ + assert.strictEqual(shouldPreventDefault, false); + assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'simpleCommand', args: [null] }]); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; diff --git a/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts b/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts index a321d9b3..f1ef51b6 100644 --- a/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts +++ b/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts @@ -11,7 +11,7 @@ suite('KeybindingLabels', () => { function assertUSLabel(OS: OperatingSystem, keybinding: number, expected: string): void { const usResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS); - assert.equal(usResolvedKeybinding.getLabel(), expected); + assert.strictEqual(usResolvedKeybinding.getLabel(), expected); } test('Windows US label', () => { @@ -116,7 +116,7 @@ suite('KeybindingLabels', () => { test('Aria label', () => { function assertAriaLabel(OS: OperatingSystem, keybinding: number, expected: string): void { const usResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS); - assert.equal(usResolvedKeybinding.getAriaLabel(), expected); + assert.strictEqual(usResolvedKeybinding.getAriaLabel(), expected); } assertAriaLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Control+Shift+Alt+Windows+A'); @@ -127,7 +127,7 @@ suite('KeybindingLabels', () => { test('Electron Accelerator label', () => { function assertElectronAcceleratorLabel(OS: OperatingSystem, keybinding: number, expected: string | null): void { const usResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS); - assert.equal(usResolvedKeybinding.getElectronAccelerator(), expected); + assert.strictEqual(usResolvedKeybinding.getElectronAccelerator(), expected); } assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Alt+Super+A'); @@ -154,7 +154,7 @@ suite('KeybindingLabels', () => { test('User Settings label', () => { function assertElectronAcceleratorLabel(OS: OperatingSystem, keybinding: number, expected: string): void { const usResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS); - assert.equal(usResolvedKeybinding.getUserSettingsLabel(), expected); + assert.strictEqual(usResolvedKeybinding.getUserSettingsLabel(), expected); } assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'ctrl+shift+alt+win+a'); diff --git a/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts b/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts index 2820e0ea..33472df8 100644 --- a/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts +++ b/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts @@ -43,12 +43,12 @@ suite('KeybindingResolver', () => { let contextRules = ContextKeyExpr.equals('bar', 'baz'); let keybindingItem = kbItem(keybinding, 'yes', null, contextRules, true); - assert.equal(KeybindingResolver.contextMatchesRules(createContext({ bar: 'baz' }), contextRules), true); - assert.equal(KeybindingResolver.contextMatchesRules(createContext({ bar: 'bz' }), contextRules), false); + assert.strictEqual(KeybindingResolver.contextMatchesRules(createContext({ bar: 'baz' }), contextRules), true); + assert.strictEqual(KeybindingResolver.contextMatchesRules(createContext({ bar: 'bz' }), contextRules), false); let resolver = new KeybindingResolver([keybindingItem], [], () => { }); - assert.equal(resolver.resolve(createContext({ bar: 'baz' }), null, getDispatchStr(runtimeKeybinding))!.commandId, 'yes'); - assert.equal(resolver.resolve(createContext({ bar: 'bz' }), null, getDispatchStr(runtimeKeybinding)), null); + assert.strictEqual(resolver.resolve(createContext({ bar: 'baz' }), null, getDispatchStr(runtimeKeybinding))!.commandId, 'yes'); + assert.strictEqual(resolver.resolve(createContext({ bar: 'bz' }), null, getDispatchStr(runtimeKeybinding)), null); }); test('resolve key with arguments', function () { @@ -59,7 +59,7 @@ suite('KeybindingResolver', () => { let keybindingItem = kbItem(keybinding, 'yes', commandArgs, contextRules, true); let resolver = new KeybindingResolver([keybindingItem], [], () => { }); - assert.equal(resolver.resolve(createContext({ bar: 'baz' }), null, getDispatchStr(runtimeKeybinding))!.commandArgs, commandArgs); + assert.strictEqual(resolver.resolve(createContext({ bar: 'baz' }), null, getDispatchStr(runtimeKeybinding))!.commandArgs, commandArgs); }); test('KeybindingResolver.combine simple 1', function () { @@ -70,7 +70,7 @@ suite('KeybindingResolver', () => { kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), false), ]); @@ -85,7 +85,7 @@ suite('KeybindingResolver', () => { kbItem(KeyCode.KEY_C, 'yes3', null, ContextKeyExpr.equals('3', 'c'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true), kbItem(KeyCode.KEY_C, 'yes3', null, ContextKeyExpr.equals('3', 'c'), false), @@ -101,7 +101,7 @@ suite('KeybindingResolver', () => { kbItem(KeyCode.KEY_A, '-yes1', null, ContextKeyExpr.equals('1', 'b'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); @@ -116,7 +116,7 @@ suite('KeybindingResolver', () => { kbItem(KeyCode.KEY_B, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); @@ -131,7 +131,7 @@ suite('KeybindingResolver', () => { kbItem(KeyCode.KEY_A, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); @@ -145,7 +145,7 @@ suite('KeybindingResolver', () => { kbItem(0, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); @@ -159,7 +159,7 @@ suite('KeybindingResolver', () => { kbItem(KeyCode.KEY_A, '-yes1', null, null!, false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); @@ -173,7 +173,7 @@ suite('KeybindingResolver', () => { kbItem(0, '-yes1', null, null!, false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); @@ -187,17 +187,17 @@ suite('KeybindingResolver', () => { kbItem(KeyCode.KEY_A, '-yes1', null, null!, false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); test('contextIsEntirelyIncluded', () => { const assertIsIncluded = (a: string | null, b: string | null) => { - assert.equal(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), true); + assert.strictEqual(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), true); }; const assertIsNotIncluded = (a: string | null, b: string | null) => { - assert.equal(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), false); + assert.strictEqual(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), false); }; assertIsIncluded('key1', null); @@ -314,11 +314,11 @@ suite('KeybindingResolver', () => { let testKey = (commandId: string, expectedKeys: number[]) => { // Test lookup let lookupResult = resolver.lookupKeybindings(commandId); - assert.equal(lookupResult.length, expectedKeys.length, 'Length mismatch @ commandId ' + commandId + '; GOT: ' + JSON.stringify(lookupResult, null, '\t')); + assert.strictEqual(lookupResult.length, expectedKeys.length, 'Length mismatch @ commandId ' + commandId + '; GOT: ' + JSON.stringify(lookupResult, null, '\t')); for (let i = 0, len = lookupResult.length; i < len; i++) { const expected = new USLayoutResolvedKeybinding(createKeybinding(expectedKeys[i], OS)!, OS); - assert.equal(lookupResult[i].resolvedKeybinding!.getUserSettingsLabel(), expected.getUserSettingsLabel(), 'value mismatch @ commandId ' + commandId); + assert.strictEqual(lookupResult[i].resolvedKeybinding!.getUserSettingsLabel(), expected.getUserSettingsLabel(), 'value mismatch @ commandId ' + commandId); } }; @@ -333,14 +333,14 @@ suite('KeybindingResolver', () => { // if it's the final part, then we should find a valid command, // and there should not be a chord. assert.ok(result !== null, `Enters chord for ${commandId} at part ${i}`); - assert.equal(result!.commandId, commandId, `Enters chord for ${commandId} at part ${i}`); - assert.equal(result!.enterChord, false, `Enters chord for ${commandId} at part ${i}`); + assert.strictEqual(result!.commandId, commandId, `Enters chord for ${commandId} at part ${i}`); + assert.strictEqual(result!.enterChord, false, `Enters chord for ${commandId} at part ${i}`); } else { // if it's not the final part, then we should not find a valid command, // and there should be a chord. assert.ok(result !== null, `Enters chord for ${commandId} at part ${i}`); - assert.equal(result!.commandId, null, `Enters chord for ${commandId} at part ${i}`); - assert.equal(result!.enterChord, true, `Enters chord for ${commandId} at part ${i}`); + assert.strictEqual(result!.commandId, null, `Enters chord for ${commandId} at part ${i}`); + assert.strictEqual(result!.enterChord, true, `Enters chord for ${commandId} at part ${i}`); } previousPart = part; } diff --git a/src/vs/platform/label/common/label.ts b/src/vs/platform/label/common/label.ts index d1739d0a..e9c0c56e 100644 --- a/src/vs/platform/label/common/label.ts +++ b/src/vs/platform/label/common/label.ts @@ -8,7 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { IWorkspace } from 'vs/platform/workspace/common/workspace'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; export const ILabelService = createDecorator('labelService'); @@ -23,7 +23,7 @@ export interface ILabelService { */ getUriLabel(resource: URI, options?: { relative?: boolean, noPrefix?: boolean, endWithSeparator?: boolean }): string; getUriBasenameLabel(resource: URI): string; - getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWorkspace), options?: { verbose: boolean }): string; + getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | IWorkspace), options?: { verbose: boolean }): string; getHostLabel(scheme: string, authority?: string): string; getSeparator(scheme: string, authority?: string): '/' | '\\'; diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index 9b3d6566..0ede527e 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -9,10 +9,9 @@ import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IWindowSettings } from 'vs/platform/windows/common/windows'; -import { OpenContext } from 'vs/platform/windows/node/window'; -import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, ICodeWindow, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { whenDeleted } from 'vs/base/node/pfs'; -import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; +import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { URI } from 'vs/base/common/uri'; import { BrowserWindow, ipcMain, Event as IpcEvent, app } from 'electron'; @@ -21,6 +20,7 @@ import { IDiagnosticInfoOptions, IDiagnosticInfo, IRemoteDiagnosticInfo, IRemote import { IMainProcessInfo, IWindowInfo } from 'vs/platform/launch/node/launch'; import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; export const ID = 'launchMainService'; export const ILaunchMainService = createDecorator(ID); @@ -35,23 +35,6 @@ export interface IRemoteDiagnosticOptions { includeWorkspaceMetadata?: boolean; } -function parseOpenUrl(args: NativeParsedArgs): { uri: URI, url: string }[] { - if (args['open-url'] && args._urls && args._urls.length > 0) { - // --open-url must contain -- followed by the url(s) - // process.argv is used over args._ as args._ are resolved to file paths at this point - return coalesce(args._urls - .map(url => { - try { - return { uri: URI.parse(url), url }; - } catch (err) { - return null; - } - })); - } - - return []; -} - export interface ILaunchMainService { readonly _serviceBrand: undefined; start(args: NativeParsedArgs, userEnv: IProcessEnvironment): Promise; @@ -68,7 +51,7 @@ export class LaunchMainService implements ILaunchMainService { @ILogService private readonly logService: ILogService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @IURLService private readonly urlService: IURLService, - @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, + @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @IConfigurationService private readonly configurationService: IConfigurationService ) { } @@ -89,7 +72,7 @@ export class LaunchMainService implements ILaunchMainService { } // Check early for open-url which is handled in URL service - const urlsToOpen = parseOpenUrl(args); + const urlsToOpen = this.parseOpenUrl(args); if (urlsToOpen.length) { let whenWindowReady: Promise = Promise.resolve(); @@ -113,7 +96,24 @@ export class LaunchMainService implements ILaunchMainService { } } - private startOpenWindow(args: NativeParsedArgs, userEnv: IProcessEnvironment): Promise { + private parseOpenUrl(args: NativeParsedArgs): { uri: URI, url: string }[] { + if (args['open-url'] && args._urls && args._urls.length > 0) { + // --open-url must contain -- followed by the url(s) + // process.argv is used over args._ as args._ are resolved to file paths at this point + return coalesce(args._urls + .map(url => { + try { + return { uri: URI.parse(url), url }; + } catch (err) { + return null; + } + })); + } + + return []; + } + + private async startOpenWindow(args: NativeParsedArgs, userEnv: IProcessEnvironment): Promise { const context = isLaunchedFromCli(userEnv) ? OpenContext.CLI : OpenContext.DESKTOP; let usedWindows: ICodeWindow[] = []; @@ -205,17 +205,15 @@ export class LaunchMainService implements ILaunchMainService { whenDeleted(waitMarkerFileURI.fsPath) ]).then(() => undefined, () => undefined); } - - return Promise.resolve(undefined); } - getMainProcessId(): Promise { + async getMainProcessId(): Promise { this.logService.trace('Received request for process ID from other instance.'); - return Promise.resolve(process.pid); + return process.pid; } - getMainProcessInfo(): Promise { + async getMainProcessInfo(): Promise { this.logService.trace('Received request for main process info from other instance.'); const windows: IWindowInfo[] = []; @@ -228,18 +226,18 @@ export class LaunchMainService implements ILaunchMainService { } }); - return Promise.resolve({ + return { mainPID: process.pid, mainArguments: process.argv.slice(1), windows, screenReader: !!app.accessibilitySupportEnabled, gpuFeatureStatus: app.getGPUFeatureStatus() - }); + }; } - getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]> { + async getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]> { const windows = this.windowsMainService.getWindows(); - const promises: Promise[] = windows.map(window => { + const diagnostics: Array = await Promise.all(windows.map(window => { return new Promise((resolve) => { const remoteAuthority = window.remoteAuthority; if (remoteAuthority) { @@ -267,27 +265,26 @@ export class LaunchMainService implements ILaunchMainService { resolve(undefined); } }); - }); + })); - return Promise.all(promises).then(diagnostics => diagnostics.filter((x): x is IRemoteDiagnosticInfo | IRemoteDiagnosticError => !!x)); + return diagnostics.filter((x): x is IRemoteDiagnosticInfo | IRemoteDiagnosticError => !!x); } private getFolderURIs(window: ICodeWindow): URI[] { const folderURIs: URI[] = []; - if (window.openedFolderUri) { - folderURIs.push(window.openedFolderUri); - } else if (window.openedWorkspace) { - // workspace folders can only be shown for local workspaces - const workspaceConfigPath = window.openedWorkspace.configPath; - const resolvedWorkspace = this.workspacesMainService.resolveLocalWorkspaceSync(workspaceConfigPath); + const workspace = window.openedWorkspace; + if (isSingleFolderWorkspaceIdentifier(workspace)) { + folderURIs.push(workspace.uri); + } else if (isWorkspaceIdentifier(workspace)) { + const resolvedWorkspace = this.workspacesManagementMainService.resolveLocalWorkspaceSync(workspace.configPath); // workspace folders can only be shown for local (resolved) workspaces if (resolvedWorkspace) { const rootFolders = resolvedWorkspace.folders; rootFolders.forEach(root => { folderURIs.push(root.uri); }); } else { - //TODO: can we add the workspace file here? + //TODO@RMacfarlane: can we add the workspace file here? } } @@ -296,13 +293,14 @@ export class LaunchMainService implements ILaunchMainService { private codeWindowToInfo(window: ICodeWindow): IWindowInfo { const folderURIs = this.getFolderURIs(window); + return this.browserWindowToInfo(window.win, folderURIs, window.remoteAuthority); } - private browserWindowToInfo(win: BrowserWindow, folderURIs: URI[] = [], remoteAuthority?: string): IWindowInfo { + private browserWindowToInfo(window: BrowserWindow, folderURIs: URI[] = [], remoteAuthority?: string): IWindowInfo { return { - pid: win.webContents.getOSProcessId(), - title: win.getTitle(), + pid: window.webContents.getOSProcessId(), + title: window.getTitle(), folderURIs, remoteAuthority }; diff --git a/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts b/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts index 62db3f75..6d82c122 100644 --- a/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts +++ b/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts @@ -576,7 +576,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe if (window && !window.isDestroyed()) { let whenWindowClosed: Promise; if (window.webContents && !window.webContents.isDestroyed()) { - whenWindowClosed = new Promise(c => window.once('closed', c)); + whenWindowClosed = new Promise(resolve => window.once('closed', resolve)); } else { whenWindowClosed = Promise.resolve(); } diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index b553a058..1d515e5b 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -429,12 +429,14 @@ export interface IResourceNavigatorOptions { export interface SelectionKeyboardEvent extends KeyboardEvent { preserveFocus?: boolean; + pinned?: boolean; __forceEvent?: boolean; } -export function getSelectionKeyboardEvent(typeArg = 'keydown', preserveFocus?: boolean): SelectionKeyboardEvent { +export function getSelectionKeyboardEvent(typeArg = 'keydown', preserveFocus?: boolean, pinned?: boolean): SelectionKeyboardEvent { const e = new KeyboardEvent(typeArg); (e).preserveFocus = preserveFocus; + (e).pinned = pinned; (e).__forceEvent = true; return e; @@ -478,8 +480,9 @@ abstract class ResourceNavigator extends Disposable { const focus = this.widget.getFocus(); this.widget.setSelection(focus, event.browserEvent); - const preserveFocus = typeof (event.browserEvent as SelectionKeyboardEvent).preserveFocus === 'boolean' ? (event.browserEvent as SelectionKeyboardEvent).preserveFocus! : true; - const pinned = !preserveFocus; + const selectionKeyboardEvent = event.browserEvent as SelectionKeyboardEvent; + const preserveFocus = typeof selectionKeyboardEvent.preserveFocus === 'boolean' ? selectionKeyboardEvent.preserveFocus! : true; + const pinned = typeof selectionKeyboardEvent.pinned === 'boolean' ? selectionKeyboardEvent.pinned! : !preserveFocus; const sideBySide = false; this._open(this.getSelectedElement(), preserveFocus, pinned, sideBySide, event.browserEvent); @@ -490,8 +493,9 @@ abstract class ResourceNavigator extends Disposable { return; } - const preserveFocus = typeof (event.browserEvent as SelectionKeyboardEvent).preserveFocus === 'boolean' ? (event.browserEvent as SelectionKeyboardEvent).preserveFocus! : true; - const pinned = !preserveFocus; + const selectionKeyboardEvent = event.browserEvent as SelectionKeyboardEvent; + const preserveFocus = typeof selectionKeyboardEvent.preserveFocus === 'boolean' ? selectionKeyboardEvent.preserveFocus! : true; + const pinned = typeof selectionKeyboardEvent.pinned === 'boolean' ? selectionKeyboardEvent.pinned! : !preserveFocus; const sideBySide = false; this._open(this.getSelectedElement(), preserveFocus, pinned, sideBySide, event.browserEvent); @@ -826,7 +830,7 @@ function workbenchTreeDataPreamble(keyboardNavigationSettingKey); + const keyboardNavigation = options.simpleKeyboardNavigation || accessibilityOn ? 'simple' : configurationService.getValue(keyboardNavigationSettingKey); const horizontalScrolling = options.horizontalScrolling !== undefined ? options.horizontalScrolling : configurationService.getValue(horizontalScrollingKey); const [workbenchListOptions, disposable] = toWorkbenchListOptions(options, configurationService, keybindingService); const additionalScrollHeight = options.additionalScrollHeight; diff --git a/src/vs/platform/localizations/common/localizations.ts b/src/vs/platform/localizations/common/localizations.ts index 509c4ada..27fa81ba 100644 --- a/src/vs/platform/localizations/common/localizations.ts +++ b/src/vs/platform/localizations/common/localizations.ts @@ -25,6 +25,8 @@ export interface ILocalizationsService { readonly onDidLanguagesChange: Event; getLanguageIds(): Promise; + + update(): Promise; } export function isValidLocalization(localization: ILocalization): boolean { diff --git a/src/vs/platform/log/node/loggerService.ts b/src/vs/platform/log/node/loggerService.ts index 7120d99d..49fe8ad4 100644 --- a/src/vs/platform/log/node/loggerService.ts +++ b/src/vs/platform/log/node/loggerService.ts @@ -9,8 +9,8 @@ import { URI } from 'vs/base/common/uri'; import { basename, extname, dirname } from 'vs/base/common/resources'; import { Schemas } from 'vs/base/common/network'; import { FileLogService } from 'vs/platform/log/common/fileLogService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { SpdLogService } from 'vs/platform/log/node/spdlogService'; +import { IFileService } from 'vs/platform/files/common/files'; export class LoggerService extends Disposable implements ILoggerService { @@ -20,7 +20,7 @@ export class LoggerService extends Disposable implements ILoggerService { constructor( @ILogService private logService: ILogService, - @IInstantiationService private instantiationService: IInstantiationService, + @IFileService private fileService: IFileService ) { super(); this._register(logService.onDidChangeLogLevel(level => this.loggers.forEach(logger => logger.setLevel(level)))); @@ -34,7 +34,7 @@ export class LoggerService extends Disposable implements ILoggerService { const ext = extname(resource); logger = new SpdLogService(baseName.substring(0, baseName.length - ext.length), dirname(resource).fsPath, this.logService.getLevel()); } else { - logger = this.instantiationService.createInstance(FileLogService, basename(resource), resource, this.logService.getLevel()); + logger = new FileLogService(basename(resource), resource, this.logService.getLevel(), this.fileService); } this.loggers.set(resource.toString(), logger); } diff --git a/src/vs/platform/markers/common/markerService.ts b/src/vs/platform/markers/common/markerService.ts index 537ade06..1c0c67c2 100644 --- a/src/vs/platform/markers/common/markerService.ts +++ b/src/vs/platform/markers/common/markerService.ts @@ -145,14 +145,11 @@ export class MarkerService implements IMarkerService { readonly onMarkerChanged: Event = Event.debounce(this._onMarkerChanged.event, MarkerService._debouncer, 0); private readonly _data = new DoubleResourceMap(); - private readonly _stats: MarkerStats; - - constructor() { - this._stats = new MarkerStats(this); - } + private readonly _stats = new MarkerStats(this); dispose(): void { this._stats.dispose(); + this._onMarkerChanged.dispose(); } getStatistics(): MarkerStatistics { diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index 3039b01a..32f6b799 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -6,9 +6,8 @@ import * as nls from 'vs/nls'; import { isMacintosh, language } from 'vs/base/common/platform'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; -import { app, Menu, MenuItem, BrowserWindow, MenuItemConstructorOptions, WebContents, Event, KeyboardEvent } from 'electron'; +import { app, Menu, MenuItem, BrowserWindow, MenuItemConstructorOptions, WebContents, KeyboardEvent } from 'electron'; import { getTitleBarStyle, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, IWindowOpenable } from 'vs/platform/windows/common/windows'; -import { OpenContext } from 'vs/platform/windows/node/window'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUpdateService, StateType } from 'vs/platform/update/common/update'; @@ -16,7 +15,7 @@ import product from 'vs/platform/product/common/product'; import { RunOnceScheduler } from 'vs/base/common/async'; import { ILogService } from 'vs/platform/log/common/log'; import { mnemonicMenuLabel } from 'vs/base/common/labels'; -import { IWindowsMainService, IWindowsCountChangedEvent } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, IWindowsCountChangedEvent, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; import { IMenubarData, IMenubarKeybinding, MenubarMenuItem, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, isMenubarMenuItemAction, IMenubarMenu, isMenubarMenuItemUriAction } from 'vs/platform/menubar/common/menubar'; import { URI } from 'vs/base/common/uri'; @@ -62,7 +61,7 @@ export class Menubar { private keybindings: { [commandId: string]: IMenubarKeybinding }; - private readonly fallbackMenuHandlers: { [id: string]: (menuItem: MenuItem, browserWindow: BrowserWindow | undefined, event: Event) => void } = Object.create(null); + private readonly fallbackMenuHandlers: { [id: string]: (menuItem: MenuItem, browserWindow: BrowserWindow | undefined, event: KeyboardEvent) => void } = Object.create(null); constructor( @IUpdateService private readonly updateService: IUpdateService, @@ -608,45 +607,18 @@ export class Menubar { } } - private static _menuItemIsTriggeredViaKeybinding(event: KeyboardEvent, userSettingsLabel: string): boolean { - // The event coming in from Electron will inform us only about the modifier keys pressed. - // The strategy here is to check if the modifier keys match those of the keybinding, - // since it is highly unlikely to use modifier keys when clicking with the mouse - if (!userSettingsLabel) { - // There is no keybinding - return false; - } - - let ctrlRequired = /ctrl/.test(userSettingsLabel); - let shiftRequired = /shift/.test(userSettingsLabel); - let altRequired = /alt/.test(userSettingsLabel); - let metaRequired = /cmd/.test(userSettingsLabel) || /super/.test(userSettingsLabel); - - if (!ctrlRequired && !shiftRequired && !altRequired && !metaRequired) { - // This keybinding does not use any modifier keys, so we cannot use this heuristic - return false; - } - - return ( - ctrlRequired === event.ctrlKey - && shiftRequired === event.shiftKey - && altRequired === event.altKey - && metaRequired === event.metaKey - ); - } - private createMenuItem(label: string, commandId: string | string[], enabled?: boolean, checked?: boolean): MenuItem; private createMenuItem(label: string, click: () => void, enabled?: boolean, checked?: boolean): MenuItem; private createMenuItem(arg1: string, arg2: any, arg3?: boolean, arg4?: boolean): MenuItem { const label = this.mnemonicLabel(arg1); - const click: () => void = (typeof arg2 === 'function') ? arg2 : (menuItem: MenuItem & IMenuItemWithKeybinding, win: BrowserWindow, event: Event) => { + const click: () => void = (typeof arg2 === 'function') ? arg2 : (menuItem: MenuItem & IMenuItemWithKeybinding, win: BrowserWindow, event: KeyboardEvent) => { const userSettingsLabel = menuItem ? menuItem.userSettingsLabel : null; let commandId = arg2; if (Array.isArray(arg2)) { commandId = this.isOptionClick(event) ? arg2[1] : arg2[0]; // support alternative action if we got multiple action Ids and the option key was pressed while invoking } - if (userSettingsLabel && Menubar._menuItemIsTriggeredViaKeybinding(event, userSettingsLabel)) { + if (userSettingsLabel && event.triggeredByAccelerator) { this.runActionInRenderer({ type: 'keybinding', userSettingsLabel }); } else { this.runActionInRenderer({ type: 'commandId', commandId }); @@ -706,8 +678,8 @@ export class Menubar { return new MenuItem(this.withKeybinding(commandId, options)); } - private makeContextAwareClickHandler(click: () => void, contextSpecificHandlers: IMenuItemClickHandler): () => void { - return () => { + private makeContextAwareClickHandler(click: (menuItem: MenuItem, win: BrowserWindow, event: KeyboardEvent) => void, contextSpecificHandlers: IMenuItemClickHandler): (menuItem: MenuItem, win: BrowserWindow | undefined, event: KeyboardEvent) => void { + return (menuItem: MenuItem, win: BrowserWindow | undefined, event: KeyboardEvent) => { // No Active Window const activeWindow = BrowserWindow.getFocusedWindow(); @@ -722,7 +694,7 @@ export class Menubar { } // Finally execute command in Window - click(); + click(menuItem, win || activeWindow, event); }; } diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 60baf2da..2c8414ba 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -49,7 +49,7 @@ export interface ICommonNativeHostService { readonly onDidChangeColorScheme: Event; - readonly onDidChangePassword: Event; + readonly onDidChangePassword: Event<{ service: string, account: string }>; // Window getWindows(): Promise; @@ -137,6 +137,7 @@ export interface ICommonNativeHostService { // Development openDevTools(options?: OpenDevToolsOptions): Promise; toggleDevTools(): Promise; + toggleSharedProcessWindow(): Promise; sendInputEvent(event: MouseInputEvent): Promise; // Connectivity diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index e24efa83..50c4460a 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -4,18 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, ICodeWindow, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, Menu, BrowserWindow, app, clipboard, powerMonitor, nativeTheme } from 'electron'; -import { OpenContext } from 'vs/platform/windows/node/window'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions, IColorScheme } from 'vs/platform/windows/common/windows'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; -import { isMacintosh, isWindows, isLinux } from 'vs/base/common/platform'; +import { isMacintosh, isWindows, isLinux, isLinuxSnap } from 'vs/base/common/platform'; import { ICommonNativeHostService, IOSProperties, IOSStatistics } from 'vs/platform/native/common/native'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { AddFirstParameterToFunctions } from 'vs/base/common/types'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { dirExists } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -28,6 +27,7 @@ import { dirname, join } from 'vs/base/common/path'; import product from 'vs/platform/product/common/product'; import { memoize } from 'vs/base/common/decorators'; import { Disposable } from 'vs/base/common/lifecycle'; +import { ISharedProcess } from 'vs/platform/sharedProcess/node/sharedProcess'; export interface INativeHostMainService extends AddFirstParameterToFunctions /* only methods, not events */, number | undefined /* window ID */> { } @@ -43,6 +43,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain declare readonly _serviceBrand: undefined; constructor( + private sharedProcess: ISharedProcess, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @IDialogMainService private readonly dialogMainService: IDialogMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @@ -91,7 +92,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain private readonly _onDidChangeColorScheme = this._register(new Emitter()); readonly onDidChangeColorScheme = this._onDidChangeColorScheme.event; - private readonly _onDidChangePassword = this._register(new Emitter()); + private readonly _onDidChangePassword = this._register(new Emitter<{ account: string, service: string }>()); readonly onDidChangePassword = this._onDidChangePassword.event; //#endregion @@ -104,7 +105,6 @@ export class NativeHostMainService extends Disposable implements INativeHostMain return windows.map(window => ({ id: window.id, workspace: window.openedWorkspace, - folderUri: window.openedFolderUri, title: window.win.getTitle(), filename: window.getRepresentedFilename(), dirty: window.isDocumentEdited() @@ -334,8 +334,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } async openExternal(windowId: number | undefined, url: string): Promise { - if (isLinux && process.env.SNAP && process.env.SNAP_REVISION) { - NativeHostMainService._safeSnapOpenExternal(url); + if (isLinuxSnap) { + this.safeSnapOpenExternal(url); } else { shell.openExternal(url); } @@ -343,7 +343,9 @@ export class NativeHostMainService extends Disposable implements INativeHostMain return true; } - private static _safeSnapOpenExternal(url: string): void { + private safeSnapOpenExternal(url: string): void { + + // Remove some environment variables before opening to avoid issues... const gdkPixbufModuleFile = process.env['GDK_PIXBUF_MODULE_FILE']; const gdkPixbufModuleDir = process.env['GDK_PIXBUF_MODULEDIR']; delete process.env['GDK_PIXBUF_MODULE_FILE']; @@ -351,6 +353,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain shell.openExternal(url); + // ...but restore them after process.env['GDK_PIXBUF_MODULE_FILE'] = gdkPixbufModuleFile; process.env['GDK_PIXBUF_MODULEDIR'] = gdkPixbufModuleDir; } @@ -626,6 +629,10 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } } + async toggleSharedProcessWindow(): Promise { + return this.sharedProcess.toggle(); + } + //#endregion //#region Registry (windows) @@ -703,7 +710,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain await keytar.setPassword(service, account, password); } - this._onDidChangePassword.fire(); + this._onDidChangePassword.fire({ service, account }); } async deletePassword(windowId: number | undefined, service: string, account: string): Promise { @@ -711,7 +718,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain const didDelete = await keytar.deletePassword(service, account); if (didDelete) { - this._onDidChangePassword.fire(); + this._onDidChangePassword.fire({ service, account }); } return didDelete; diff --git a/src/vs/platform/opener/browser/link.ts b/src/vs/platform/opener/browser/link.ts index 7b6863cd..ca887940 100644 --- a/src/vs/platform/opener/browser/link.ts +++ b/src/vs/platform/opener/browser/link.ts @@ -20,11 +20,13 @@ export interface ILinkDescriptor { export interface ILinkStyles { readonly textLinkForeground?: Color; + readonly disabled?: boolean; } export class Link extends Disposable { readonly el: HTMLAnchorElement; + private disabled: boolean; private styles: ILinkStyles = { textLinkForeground: Color.fromHex('#006AB1') }; @@ -50,9 +52,12 @@ export class Link extends Disposable { this._register(onOpen(e => { EventHelper.stop(e, true); - openerService.open(link.href); + if (!this.disabled) { + openerService.open(link.href); + } })); + this.disabled = false; this.applyStyles(); } @@ -62,6 +67,26 @@ export class Link extends Disposable { } private applyStyles(): void { - this.el.style.color = this.styles.textLinkForeground?.toString() || ''; + const color = this.styles.textLinkForeground?.toString(); + if (color) { + this.el.style.color = color; + } + if (typeof this.styles.disabled === 'boolean' && this.styles.disabled !== this.disabled) { + if (this.styles.disabled) { + this.el.setAttribute('aria-disabled', 'true'); + this.el.tabIndex = -1; + this.el.style.pointerEvents = 'none'; + this.el.style.opacity = '0.4'; + this.el.style.cursor = 'default'; + this.disabled = true; + } else { + this.el.setAttribute('aria-disabled', 'false'); + this.el.tabIndex = 0; + this.el.style.pointerEvents = 'auto'; + this.el.style.opacity = '1'; + this.el.style.cursor = 'pointer'; + this.disabled = false; + } + } } } diff --git a/src/vs/platform/opener/common/opener.ts b/src/vs/platform/opener/common/opener.ts index d82651f9..ff6740cf 100644 --- a/src/vs/platform/opener/common/opener.ts +++ b/src/vs/platform/opener/common/opener.ts @@ -3,15 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { equalsIgnoreCase, startsWithIgnoreCase } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export const IOpenerService = createDecorator('openerService'); -type OpenInternalOptions = { +export type OpenInternalOptions = { /** * Signals that the intent is to open an editor to the side @@ -31,7 +32,11 @@ type OpenInternalOptions = { readonly fromUserGesture?: boolean; }; -type OpenExternalOptions = { readonly openExternal?: boolean; readonly allowTunneling?: boolean }; +export type OpenExternalOptions = { + readonly openExternal?: boolean; + readonly allowTunneling?: boolean; + readonly allowContributedOpeners?: boolean | string; +}; export type OpenOptions = OpenInternalOptions & OpenExternalOptions; @@ -46,7 +51,8 @@ export interface IOpener { } export interface IExternalOpener { - openExternal(href: string): Promise; + openExternal(href: string, ctx: { sourceUri: URI, preferredOpenerId?: string }, token: CancellationToken): Promise; + dispose?(): void; } export interface IValidator { @@ -81,7 +87,12 @@ export interface IOpenerService { * Sets the handler for opening externally. If not provided, * a default handler will be used. */ - setExternalOpener(opener: IExternalOpener): void; + setDefaultExternalOpener(opener: IExternalOpener): void; + + /** + * Registers a new opener external resources openers. + */ + registerExternalOpener(opener: IExternalOpener): IDisposable; /** * Opens a resource, like a webaddress, a document uri, or executes command. @@ -97,15 +108,16 @@ export interface IOpenerService { resolveExternalUri(resource: URI, options?: ResolveExternalUriOptions): Promise; } -export const NullOpenerService: IOpenerService = Object.freeze({ +export const NullOpenerService = Object.freeze({ _serviceBrand: undefined, registerOpener() { return Disposable.None; }, registerValidator() { return Disposable.None; }, registerExternalUriResolver() { return Disposable.None; }, - setExternalOpener() { }, + setDefaultExternalOpener() { }, + registerExternalOpener() { return Disposable.None; }, async open() { return false; }, async resolveExternalUri(uri: URI) { return { resolved: uri, dispose() { } }; }, -}); +} as IOpenerService); export function matchesScheme(target: URI | string, scheme: string) { if (URI.isUri(target)) { diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 56fbc68a..537db557 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -20,7 +20,7 @@ if (isWeb || typeof require === 'undefined' || typeof require.__$__nodeRequire ! // Running out of sources if (Object.keys(product).length === 0) { Object.assign(product, { - version: '1.52.0-dev', + version: '1.53.0-dev', nameShort: isWeb ? 'Code Web - OSS Dev' : 'Code - OSS Dev', nameLong: isWeb ? 'Code Web - OSS Dev' : 'Code - OSS Dev', applicationName: 'code-oss', diff --git a/src/vs/platform/product/common/productService.ts b/src/vs/platform/product/common/productService.ts index 74a8c744..92b8c1a6 100644 --- a/src/vs/platform/product/common/productService.ts +++ b/src/vs/platform/product/common/productService.ts @@ -129,6 +129,8 @@ export interface IProductConfiguration { readonly linkProtectionTrustedDomains?: readonly string[]; readonly 'configurationSync.store'?: ConfigurationSyncStore; + + readonly darwinUniversalAssetId?: string; } export type ImportantExtensionTip = { name: string; languages?: string[]; pattern?: string; isExtensionPack?: boolean }; diff --git a/src/vs/platform/remote/browser/browserSocketFactory.ts b/src/vs/platform/remote/browser/browserSocketFactory.ts index 3715cbb8..2f343e84 100644 --- a/src/vs/platform/remote/browser/browserSocketFactory.ts +++ b/src/vs/platform/remote/browser/browserSocketFactory.ts @@ -208,7 +208,7 @@ export class BrowserSocketFactory implements ISocketFactory { } connect(host: string, port: number, query: string, callback: IConnectCallback): void { - const socket = this._webSocketFactory.create(`ws://${host}:${port}/?${query}&skipWebSocketFrames=false`); + const socket = this._webSocketFactory.create(`ws://${/:/.test(host) ? `[${host}]` : host}:${port}/?${query}&skipWebSocketFrames=false`); const errorListener = socket.onError((err) => callback(err, undefined)); socket.onOpen(() => { errorListener.dispose(); diff --git a/src/vs/platform/remote/common/remoteAgentConnection.ts b/src/vs/platform/remote/common/remoteAgentConnection.ts index fdd5890c..82791249 100644 --- a/src/vs/platform/remote/common/remoteAgentConnection.ts +++ b/src/vs/platform/remote/common/remoteAgentConnection.ts @@ -8,7 +8,7 @@ import { generateUuid } from 'vs/base/common/uuid'; import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { Disposable } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { RemoteAuthorityResolverError } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors'; import { ISignService } from 'vs/platform/sign/common/sign'; @@ -86,96 +86,124 @@ export interface ISocketFactory { connect(host: string, port: number, query: string, callback: IConnectCallback): void; } -async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined): Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }> { - const logPrefix = connectLogPrefix(options, connectionType); - const { protocol, ownsProtocol } = await new Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }>((c, e) => { - options.logService.trace(`${logPrefix} 1/6. invoking socketFactory.connect().`); - options.socketFactory.connect( - options.host, - options.port, - `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`, - (err: any, socket: ISocket | undefined) => { - if (err || !socket) { - options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`); - options.logService.error(err); - e(err); - return; - } +async function readOneControlMessage(protocol: PersistentProtocol): Promise { + const raw = await Event.toPromise(protocol.onControlMessage); + const msg = JSON.parse(raw.toString()); + const error = getErrorFromMessage(msg); + if (error) { + throw error; + } + return msg; +} - options.logService.trace(`${logPrefix} 2/6. socketFactory.connect() was successful.`); - if (options.reconnectionProtocol) { - options.reconnectionProtocol.beginAcceptReconnection(socket, null); - c({ protocol: options.reconnectionProtocol, ownsProtocol: false }); - } else { - c({ protocol: new PersistentProtocol(socket, null), ownsProtocol: true }); - } +function waitWithTimeout(promise: Promise, timeout: number): Promise { + return new Promise((resolve, reject) => { + const timeoutToken = setTimeout(() => { + const error: any = new Error('Timeout'); + error.code = 'ETIMEDOUT'; + error.syscall = 'connect'; + reject(error); + }, timeout); + + promise.then( + (result) => { + clearTimeout(timeoutToken); + resolve(result); + }, + (error) => { + clearTimeout(timeoutToken); + reject(error); } ); }); +} - return new Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }>((c, e) => { +function createSocket(socketFactory: ISocketFactory, host: string, port: number, query: string): Promise { + return new Promise((resolve, reject) => { + socketFactory.connect(host, port, query, (err: any, socket: ISocket | undefined) => { + if (err || !socket) { + return reject(err); + } + resolve(socket); + }); + }); +} - const errorTimeoutToken = setTimeout(() => { - const error: any = new Error('handshake timeout'); - error.code = 'ETIMEDOUT'; - error.syscall = 'connect'; +async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined): Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }> { + const logPrefix = connectLogPrefix(options, connectionType); + + options.logService.trace(`${logPrefix} 1/6. invoking socketFactory.connect().`); + + let socket: ISocket; + try { + socket = await createSocket(options.socketFactory, options.host, options.port, `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`); + } catch (error) { + options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`); + options.logService.error(error); + throw error; + } + + options.logService.trace(`${logPrefix} 2/6. socketFactory.connect() was successful.`); + + let protocol: PersistentProtocol; + let ownsProtocol: boolean; + if (options.reconnectionProtocol) { + options.reconnectionProtocol.beginAcceptReconnection(socket, null); + protocol = options.reconnectionProtocol; + ownsProtocol = false; + } else { + protocol = new PersistentProtocol(socket, null); + ownsProtocol = true; + } + + options.logService.trace(`${logPrefix} 3/6. sending AuthRequest control message.`); + const authRequest: AuthRequest = { + type: 'auth', + auth: options.connectionToken || '00000000000000000000' + }; + protocol.sendControl(VSBuffer.fromString(JSON.stringify(authRequest))); + + try { + const msg = await waitWithTimeout(readOneControlMessage(protocol), 10000); + + if (msg.type !== 'sign' || typeof msg.data !== 'string') { + const error: any = new Error('Unexpected handshake message'); + error.code = 'VSCODE_CONNECTION_ERROR'; + throw error; + } + + options.logService.trace(`${logPrefix} 4/6. received SignRequest control message.`); + + const signed = await options.signService.sign(msg.data); + const connTypeRequest: ConnectionTypeRequest = { + type: 'connectionType', + commit: options.commit, + signedData: signed, + desiredConnectionType: connectionType + }; + if (args) { + connTypeRequest.args = args; + } + + options.logService.trace(`${logPrefix} 5/6. sending ConnectionTypeRequest control message.`); + protocol.sendControl(VSBuffer.fromString(JSON.stringify(connTypeRequest))); + + return { protocol, ownsProtocol }; + + } catch (error) { + if (error && error.code === 'ETIMEDOUT') { options.logService.error(`${logPrefix} the handshake took longer than 10 seconds. Error:`); options.logService.error(error); - if (ownsProtocol) { - safeDisposeProtocolAndSocket(protocol); - } - e(error); - }, 10000); - - const messageRegistration = protocol.onControlMessage(async raw => { - const msg = JSON.parse(raw.toString()); - // Stop listening for further events - messageRegistration.dispose(); - - const error = getErrorFromMessage(msg); - if (error) { - options.logService.error(`${logPrefix} received error control message when negotiating connection. Error:`); - options.logService.error(error); - if (ownsProtocol) { - safeDisposeProtocolAndSocket(protocol); - } - return e(error); - } - - if (msg.type === 'sign') { - options.logService.trace(`${logPrefix} 4/6. received SignRequest control message.`); - const signed = await options.signService.sign(msg.data); - const connTypeRequest: ConnectionTypeRequest = { - type: 'connectionType', - commit: options.commit, - signedData: signed, - desiredConnectionType: connectionType - }; - if (args) { - connTypeRequest.args = args; - } - options.logService.trace(`${logPrefix} 5/6. sending ConnectionTypeRequest control message.`); - protocol.sendControl(VSBuffer.fromString(JSON.stringify(connTypeRequest))); - clearTimeout(errorTimeoutToken); - c({ protocol, ownsProtocol }); - } else { - const error = new Error('handshake error'); - options.logService.error(`${logPrefix} received unexpected control message. Error:`); - options.logService.error(error); - if (ownsProtocol) { - safeDisposeProtocolAndSocket(protocol); - } - e(error); - } - }); - - options.logService.trace(`${logPrefix} 3/6. sending AuthRequest control message.`); - const authRequest: AuthRequest = { - type: 'auth', - auth: options.connectionToken || '00000000000000000000' - }; - protocol.sendControl(VSBuffer.fromString(JSON.stringify(authRequest))); - }); + } + if (error && error.code === 'VSCODE_CONNECTION_ERROR') { + options.logService.error(`${logPrefix} received error control message when negotiating connection. Error:`); + options.logService.error(error); + } + if (ownsProtocol) { + safeDisposeProtocolAndSocket(protocol); + } + throw error; + } } interface IManagementConnectionResult { @@ -287,7 +315,7 @@ export async function connectRemoteAgentManagement(options: IConnectionOptions, } catch (err) { options.logService.error(`[remote-connection] An error occurred in the very first connect attempt, it will be treated as a permanent error! Error:`); options.logService.error(err); - PersistentConnection.triggerPermanentFailure(); + PersistentConnection.triggerPermanentFailure(0, 0, RemoteAuthorityResolverError.isHandled(err)); throw err; } } @@ -301,7 +329,7 @@ export async function connectRemoteAgentExtensionHost(options: IConnectionOption } catch (err) { options.logService.error(`[remote-connection] An error occurred in the very first connect attempt, it will be treated as a permanent error! Error:`); options.logService.error(err); - PersistentConnection.triggerPermanentFailure(); + PersistentConnection.triggerPermanentFailure(0, 0, RemoteAuthorityResolverError.isHandled(err)); throw err; } } @@ -333,10 +361,16 @@ export const enum PersistentConnectionEventType { } export class ConnectionLostEvent { public readonly type = PersistentConnectionEventType.ConnectionLost; + constructor( + public readonly reconnectionToken: string, + public readonly millisSinceLastIncomingData: number + ) { } } export class ReconnectionWaitEvent { public readonly type = PersistentConnectionEventType.ReconnectionWait; constructor( + public readonly reconnectionToken: string, + public readonly millisSinceLastIncomingData: number, public readonly durationSeconds: number, private readonly cancellableTimer: CancelablePromise ) { } @@ -347,22 +381,44 @@ export class ReconnectionWaitEvent { } export class ReconnectionRunningEvent { public readonly type = PersistentConnectionEventType.ReconnectionRunning; + constructor( + public readonly reconnectionToken: string, + public readonly millisSinceLastIncomingData: number, + public readonly attempt: number + ) { } } export class ConnectionGainEvent { public readonly type = PersistentConnectionEventType.ConnectionGain; + constructor( + public readonly reconnectionToken: string, + public readonly millisSinceLastIncomingData: number, + public readonly attempt: number + ) { } } export class ReconnectionPermanentFailureEvent { public readonly type = PersistentConnectionEventType.ReconnectionPermanentFailure; + constructor( + public readonly reconnectionToken: string, + public readonly millisSinceLastIncomingData: number, + public readonly attempt: number, + public readonly handled: boolean + ) { } } export type PersistentConnectionEvent = ConnectionGainEvent | ConnectionLostEvent | ReconnectionWaitEvent | ReconnectionRunningEvent | ReconnectionPermanentFailureEvent; abstract class PersistentConnection extends Disposable { - public static triggerPermanentFailure(): void { + public static triggerPermanentFailure(millisSinceLastIncomingData: number, attempt: number, handled: boolean): void { this._permanentFailure = true; - this._instances.forEach(instance => instance._gotoPermanentFailure()); + this._permanentFailureMillisSinceLastIncomingData = millisSinceLastIncomingData; + this._permanentFailureAttempt = attempt; + this._permanentFailureHandled = handled; + this._instances.forEach(instance => instance._gotoPermanentFailure(this._permanentFailureMillisSinceLastIncomingData, this._permanentFailureAttempt, this._permanentFailureHandled)); } private static _permanentFailure: boolean = false; + private static _permanentFailureMillisSinceLastIncomingData: number = 0; + private static _permanentFailureAttempt: number = 0; + private static _permanentFailureHandled: boolean = false; private static _instances: PersistentConnection[] = []; private readonly _onDidStateChange = this._register(new Emitter()); @@ -381,7 +437,7 @@ abstract class PersistentConnection extends Disposable { this.protocol = protocol; this._isReconnecting = false; - this._onDidStateChange.fire(new ConnectionGainEvent()); + this._onDidStateChange.fire(new ConnectionGainEvent(this.reconnectionToken, 0, 0)); this._register(protocol.onSocketClose(() => this._beginReconnecting())); this._register(protocol.onSocketTimeout(() => this._beginReconnecting())); @@ -389,7 +445,7 @@ abstract class PersistentConnection extends Disposable { PersistentConnection._instances.push(this); if (PersistentConnection._permanentFailure) { - this._gotoPermanentFailure(); + this._gotoPermanentFailure(PersistentConnection._permanentFailureMillisSinceLastIncomingData, PersistentConnection._permanentFailureAttempt, PersistentConnection._permanentFailureHandled); } } @@ -413,21 +469,23 @@ abstract class PersistentConnection extends Disposable { } const logPrefix = commonLogPrefix(this._connectionType, this.reconnectionToken, true); this._options.logService.info(`${logPrefix} starting reconnecting loop. You can get more information with the trace log level.`); - this._onDidStateChange.fire(new ConnectionLostEvent()); - const TIMES = [5, 5, 10, 10, 10, 10, 10, 30]; + this._onDidStateChange.fire(new ConnectionLostEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData())); + const TIMES = [0, 5, 5, 10, 10, 10, 10, 10, 30]; const disconnectStartTime = Date.now(); let attempt = -1; do { attempt++; const waitTime = (attempt < TIMES.length ? TIMES[attempt] : TIMES[TIMES.length - 1]); try { - const sleepPromise = sleep(waitTime); - this._onDidStateChange.fire(new ReconnectionWaitEvent(waitTime, sleepPromise)); + if (waitTime > 0) { + const sleepPromise = sleep(waitTime); + this._onDidStateChange.fire(new ReconnectionWaitEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), waitTime, sleepPromise)); - this._options.logService.info(`${logPrefix} waiting for ${waitTime} seconds before reconnecting...`); - try { - await sleepPromise; - } catch { } // User canceled timer + this._options.logService.info(`${logPrefix} waiting for ${waitTime} seconds before reconnecting...`); + try { + await sleepPromise; + } catch { } // User canceled timer + } if (PersistentConnection._permanentFailure) { this._options.logService.error(`${logPrefix} permanent failure occurred while running the reconnecting loop.`); @@ -435,26 +493,26 @@ abstract class PersistentConnection extends Disposable { } // connection was lost, let's try to re-establish it - this._onDidStateChange.fire(new ReconnectionRunningEvent()); + this._onDidStateChange.fire(new ReconnectionRunningEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), attempt + 1)); this._options.logService.info(`${logPrefix} resolving connection...`); const simpleOptions = await resolveConnectionOptions(this._options, this.reconnectionToken, this.protocol); this._options.logService.info(`${logPrefix} connecting to ${simpleOptions.host}:${simpleOptions.port}...`); await connectWithTimeLimit(simpleOptions.logService, this._reconnect(simpleOptions), RECONNECT_TIMEOUT); this._options.logService.info(`${logPrefix} reconnected!`); - this._onDidStateChange.fire(new ConnectionGainEvent()); + this._onDidStateChange.fire(new ConnectionGainEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), attempt + 1)); break; } catch (err) { if (err.code === 'VSCODE_CONNECTION_ERROR') { this._options.logService.error(`${logPrefix} A permanent error occurred in the reconnecting loop! Will give up now! Error:`); this._options.logService.error(err); - PersistentConnection.triggerPermanentFailure(); + PersistentConnection.triggerPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, false); break; } if (Date.now() - disconnectStartTime > ProtocolConstants.ReconnectionGraceTime) { this._options.logService.error(`${logPrefix} An error occurred while reconnecting, but it will be treated as a permanent error because the reconnection grace time has expired! Will give up now! Error:`); this._options.logService.error(err); - PersistentConnection.triggerPermanentFailure(); + PersistentConnection.triggerPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, false); break; } if (RemoteAuthorityResolverError.isTemporarilyNotAvailable(err)) { @@ -475,16 +533,22 @@ abstract class PersistentConnection extends Disposable { // try again! continue; } + if (err instanceof RemoteAuthorityResolverError) { + this._options.logService.error(`${logPrefix} A RemoteAuthorityResolverError occurred while trying to reconnect. Will give up now! Error:`); + this._options.logService.error(err); + PersistentConnection.triggerPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, RemoteAuthorityResolverError.isHandled(err)); + break; + } this._options.logService.error(`${logPrefix} An unknown error occurred while trying to reconnect, since this is an unknown case, it will be treated as a permanent error! Will give up now! Error:`); this._options.logService.error(err); - PersistentConnection.triggerPermanentFailure(); + PersistentConnection.triggerPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, false); break; } } while (!PersistentConnection._permanentFailure); } - private _gotoPermanentFailure(): void { - this._onDidStateChange.fire(new ReconnectionPermanentFailureEvent()); + private _gotoPermanentFailure(millisSinceLastIncomingData: number, attempt: number, handled: boolean): void { + this._onDidStateChange.fire(new ReconnectionPermanentFailureEvent(this.reconnectionToken, millisSinceLastIncomingData, attempt, handled)); safeDisposeProtocolAndSocket(this.protocol); } diff --git a/src/vs/platform/remote/common/remoteAgentEnvironment.ts b/src/vs/platform/remote/common/remoteAgentEnvironment.ts index 3052141f..fd275280 100644 --- a/src/vs/platform/remote/common/remoteAgentEnvironment.ts +++ b/src/vs/platform/remote/common/remoteAgentEnvironment.ts @@ -5,6 +5,7 @@ import { URI } from 'vs/base/common/uri'; import { OperatingSystem } from 'vs/base/common/platform'; +import * as performance from 'vs/base/common/performance'; export interface IRemoteAgentEnvironment { pid: number; @@ -18,6 +19,7 @@ export interface IRemoteAgentEnvironment { workspaceStorageHome: URI; userHome: URI; os: OperatingSystem; + marks: performance.PerformanceMark[]; } export interface RemoteAgentConnectionContext { diff --git a/src/vs/platform/remote/common/remoteHosts.ts b/src/vs/platform/remote/common/remoteHosts.ts index 1f3ab36f..86f2c4f2 100644 --- a/src/vs/platform/remote/common/remoteHosts.ts +++ b/src/vs/platform/remote/common/remoteHosts.ts @@ -19,7 +19,7 @@ export function getRemoteName(authority: string | undefined): string | undefined } const pos = authority.indexOf('+'); if (pos < 0) { - // funky? bad authority? + // e.g. localhost:8000 return authority; } return authority.substr(0, pos); diff --git a/src/vs/platform/remote/common/tunnel.ts b/src/vs/platform/remote/common/tunnel.ts index 980c3203..b4009323 100644 --- a/src/vs/platform/remote/common/tunnel.ts +++ b/src/vs/platform/remote/common/tunnel.ts @@ -5,6 +5,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { isWindows, OperatingSystem } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; @@ -17,36 +18,64 @@ export interface RemoteTunnel { readonly tunnelRemoteHost: string; readonly tunnelLocalPort?: number; readonly localAddress: string; - dispose(silent?: boolean): void; + readonly public: boolean; + dispose(silent?: boolean): Promise; } export interface TunnelOptions { - remoteAddress: { port: number, host: string }; + remoteAddress: { port: number, host: string; }; localAddressPort?: number; label?: string; + public?: boolean; } export interface TunnelCreationOptions { elevationRequired?: boolean; } +export interface TunnelProviderFeatures { + elevation: boolean; + public: boolean; +} + export interface ITunnelProvider { - forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise | undefined; + forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise | undefined; +} + +export interface ITunnel { + remoteAddress: { port: number, host: string }; + + /** + * The complete local address(ex. localhost:1234) + */ + localAddress: string; + + public?: boolean; + + /** + * Implementers of Tunnel should fire onDidDispose when dispose is called. + */ + onDidDispose: Event; + + dispose(): Promise | void; } export interface ITunnelService { readonly _serviceBrand: undefined; readonly tunnels: Promise; + readonly canMakePublic: boolean; readonly onTunnelOpened: Event; - readonly onTunnelClosed: Event<{ host: string, port: number }>; + readonly onTunnelClosed: Event<{ host: string, port: number; }>; + readonly canElevate: boolean; - openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number): Promise | undefined; + canTunnel(uri: URI): boolean; + openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded?: boolean, isPublic?: boolean): Promise | undefined; closeTunnel(remoteHost: string, remotePort: number): Promise; - setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable; + setTunnelProvider(provider: ITunnelProvider | undefined, features: TunnelProviderFeatures): IDisposable; } -export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: string, port: number } | undefined { +export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: string, port: number; } | undefined { if (uri.scheme !== 'http' && uri.scheme !== 'https') { return undefined; } @@ -74,51 +103,88 @@ function getOtherLocalhost(host: string): string | undefined { return (host === 'localhost') ? '127.0.0.1' : ((host === '127.0.0.1') ? 'localhost' : undefined); } +export function isPortPrivileged(port: number, os?: OperatingSystem): boolean { + if (os) { + return os !== OperatingSystem.Windows && (port < 1024); + } else { + return !isWindows && (port < 1024); + } +} + export abstract class AbstractTunnelService implements ITunnelService { declare readonly _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 }>>(); + 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; + protected _canElevate: boolean = false; + private _canMakePublic: boolean = false; public constructor( @ILogService protected readonly logService: ILogService ) { } - setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable { + setTunnelProvider(provider: ITunnelProvider | undefined, features: TunnelProviderFeatures): IDisposable { + this._tunnelProvider = provider; if (!provider) { + // clear features + this._canElevate = false; + this._canMakePublic = false; return { dispose: () => { } }; } - this._tunnelProvider = provider; + this._canElevate = features.elevation; + this._canMakePublic = features.public; return { dispose: () => { this._tunnelProvider = undefined; + this._canElevate = false; + this._canMakePublic = false; } }; } - 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); + public get canElevate(): boolean { + return this._canElevate; } - dispose(): void { + public get canMakePublic() { + return this._canMakePublic; + } + + public get tunnels(): Promise { + return new Promise(async (resolve) => { + const tunnels: RemoteTunnel[] = []; + const tunnelArray = Array.from(this._tunnels.values()); + for (let portMap of tunnelArray) { + const portArray = Array.from(portMap.values()); + for (let x of portArray) { + const tunnelValue = await x.value; + if (tunnelValue) { + tunnels.push(tunnelValue); + } + } + } + resolve(tunnels); + }); + } + + async dispose(): Promise { for (const portMap of this._tunnels.values()) { for (const { value } of portMap.values()) { - value.then(tunnel => tunnel.dispose()); + await value.then(tunnel => tunnel?.dispose()); } portMap.clear(); } this._tunnels.clear(); } - openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort: number): Promise | undefined { + openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded: boolean = false, isPublic: boolean = false): Promise | undefined { + this.logService.trace(`openTunnel request for ${remoteHost}:${remotePort} on local port ${localPort}.`); if (!addressProvider) { return undefined; } @@ -127,12 +193,19 @@ export abstract class AbstractTunnelService implements ITunnelService { remoteHost = 'localhost'; } - const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort); + const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort, elevateIfNeeded, isPublic); if (!resolvedTunnel) { + this.logService.trace(`Tunnel was not created.`); return resolvedTunnel; } return resolvedTunnel.then(tunnel => { + if (!tunnel) { + this.logService.trace('New tunnel is undefined.'); + this.removeEmptyTunnelFromMap(remoteHost!, remotePort); + return undefined; + } + this.logService.trace('New tunnel established.'); 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.'); @@ -148,24 +221,28 @@ export abstract class AbstractTunnelService implements ITunnelService { tunnelRemoteHost: tunnel.tunnelRemoteHost, tunnelLocalPort: tunnel.tunnelLocalPort, localAddress: tunnel.localAddress, - dispose: () => { + public: tunnel.public, + dispose: async () => { 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); + await this.tryDisposeTunnel(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort, existing); } } } }; } - private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number, readonly value: Promise }): Promise { + 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(true); - this._onTunnelClosed.fire({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }); + this.logService.trace(`Tunnel is being disposed ${remoteHost}:${remotePort}.`); + const disposePromise: Promise = tunnel.value.then(async (tunnel) => { + if (tunnel) { + await tunnel.dispose(true); + this._onTunnelClosed.fire({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }); + } }); if (this._tunnels.has(remoteHost)) { this._tunnels.get(remoteHost)!.delete(remotePort); @@ -183,16 +260,30 @@ export abstract class AbstractTunnelService implements ITunnelService { } } - protected addTunnelToMap(remoteHost: string, remotePort: number, tunnel: Promise) { + 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 getTunnelFromMap(remoteHost: string, remotePort: number): { refcount: number, readonly value: Promise } | undefined { + private async removeEmptyTunnelFromMap(remoteHost: string, remotePort: number) { + const hostMap = this._tunnels.get(remoteHost); + if (hostMap) { + const tunnel = hostMap.get(remotePort); + const tunnelResult = await tunnel; + if (!tunnelResult) { + hostMap.delete(remotePort); + } + if (hostMap.size === 0) { + this._tunnels.delete(remoteHost); + } + } + } + + protected getTunnelFromMap(remoteHost: string, remotePort: number): { refcount: number, readonly value: Promise } | undefined { const otherLocalhost = getOtherLocalhost(remoteHost); - let portMap: Map }> | undefined; + let portMap: Map }> | undefined; if (otherLocalhost) { const firstMap = this._tunnels.get(remoteHost); const secondMap = this._tunnels.get(otherLocalhost); @@ -207,31 +298,25 @@ export abstract class AbstractTunnelService implements ITunnelService { return portMap ? portMap.get(remotePort) : undefined; } - protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined; + canTunnel(uri: URI): boolean { + return !!extractLocalHostUriMetaDataForPortMapping(uri); + } - protected isPortPrivileged(port: number): boolean { - return port < 1024; + protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise | undefined; + + protected createWithProvider(tunnelProvider: ITunnelProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise | undefined { + this.logService.trace(`Creating tunnel with provider ${remoteHost}:${remotePort} on local port ${localPort}.`); + + const preferredLocalPort = localPort === undefined ? remotePort : localPort; + const creationInfo = { elevationRequired: elevateIfNeeded ? isPortPrivileged(preferredLocalPort) : false }; + const tunnelOptions: TunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort, public: isPublic }; + const tunnel = tunnelProvider.forwardPort(tunnelOptions, creationInfo); + this.logService.trace('Tunnel created by provider.'); + if (tunnel) { + this.addTunnelToMap(remoteHost, remotePort, tunnel); + } + return tunnel; } } -export class TunnelService extends AbstractTunnelService { - protected retainOrCreateTunnel(_addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number | undefined): Promise | undefined { - const existing = this.getTunnelFromMap(remoteHost, remotePort); - if (existing) { - ++existing.refcount; - return existing.value; - } - if (this._tunnelProvider) { - const preferredLocalPort = localPort === undefined ? remotePort : localPort; - const tunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort }; - const creationInfo = { elevationRequired: this.isPortPrivileged(preferredLocalPort) }; - const tunnel = this._tunnelProvider.forwardPort(tunnelOptions, creationInfo); - if (tunnel) { - this.addTunnelToMap(remoteHost, remotePort, tunnel); - } - return tunnel; - } - return undefined; - } -} diff --git a/src/vs/platform/remote/node/nodeSocketFactory.ts b/src/vs/platform/remote/node/nodeSocketFactory.ts index 44d68996..0a95e23e 100644 --- a/src/vs/platform/remote/node/nodeSocketFactory.ts +++ b/src/vs/platform/remote/node/nodeSocketFactory.ts @@ -22,7 +22,7 @@ export const nodeSocketFactory = new class implements ISocketFactory { const nonce = buffer.toString('base64'); let headers = [ - `GET ws://${host}:${port}/?${query}&skipWebSocketFrames=true HTTP/1.1`, + `GET ws://${/:/.test(host) ? `[${host}]` : host}:${port}/?${query}&skipWebSocketFrames=true HTTP/1.1`, `Connection: Upgrade`, `Upgrade: websocket`, `Sec-WebSocket-Key: ${nonce}` diff --git a/src/vs/platform/remote/node/tunnelService.ts b/src/vs/platform/remote/node/tunnelService.ts index 19ec1efd..80ef45f2 100644 --- a/src/vs/platform/remote/node/tunnelService.ts +++ b/src/vs/platform/remote/node/tunnelService.ts @@ -10,7 +10,7 @@ import { findFreePortFaster } from 'vs/base/node/ports'; import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; -import { connectRemoteAgentTunnel, IConnectionOptions, IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection'; +import { connectRemoteAgentTunnel, IConnectionOptions, IAddressProvider, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; import { AbstractTunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory'; import { ISignService } from 'vs/platform/sign/common/sign'; @@ -26,6 +26,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { public tunnelLocalPort!: number; public tunnelRemoteHost: string; public localAddress!: string; + public readonly public = false; private readonly _options: IConnectionOptions; private readonly _server: net.Server; @@ -57,7 +58,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { this.tunnelRemoteHost = tunnelRemoteHost; } - public dispose(): void { + public async dispose(): Promise { super.dispose(); this._server.removeListener('listening', this._listeningListener); this._server.removeListener('connection', this._connectionListener); @@ -129,8 +130,9 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { } } -export class TunnelService extends AbstractTunnelService { +export class BaseTunnelService extends AbstractTunnelService { public constructor( + private readonly socketFactory: ISocketFactory, @ILogService logService: ILogService, @ISignService private readonly signService: ISignService, @IProductService private readonly productService: IProductService @@ -138,7 +140,7 @@ export class TunnelService extends AbstractTunnelService { super(logService); } - protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined { + protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise | undefined { const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) { ++existing.refcount; @@ -146,18 +148,12 @@ export class TunnelService extends AbstractTunnelService { } if (this._tunnelProvider) { - const preferredLocalPort = localPort === undefined ? remotePort : localPort; - const creationInfo = { elevationRequired: this.isPortPrivileged(preferredLocalPort) }; - const tunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort }; - const tunnel = this._tunnelProvider.forwardPort(tunnelOptions, creationInfo); - if (tunnel) { - this.addTunnelToMap(remoteHost, remotePort, tunnel); - } - return tunnel; + return this.createWithProvider(this._tunnelProvider, remoteHost, remotePort, localPort, elevateIfNeeded, isPublic); } else { + this.logService.trace(`Creating tunnel without provider ${remoteHost}:${remotePort} on local port ${localPort}.`); const options: IConnectionOptions = { commit: this.productService.commit, - socketFactory: nodeSocketFactory, + socketFactory: this.socketFactory, addressProvider, signService: this.signService, logService: this.logService, @@ -165,8 +161,19 @@ export class TunnelService extends AbstractTunnelService { }; const tunnel = createRemoteTunnel(options, remoteHost, remotePort, localPort); + this.logService.trace('Tunnel created without provider.'); this.addTunnelToMap(remoteHost, remotePort, tunnel); return tunnel; } } } + +export class TunnelService extends BaseTunnelService { + public constructor( + @ILogService logService: ILogService, + @ISignService signService: ISignService, + @IProductService productService: IProductService + ) { + super(nodeSocketFactory, logService, signService, productService); + } +} diff --git a/src/vs/platform/sharedProcess/node/sharedProcess.ts b/src/vs/platform/sharedProcess/node/sharedProcess.ts new file mode 100644 index 00000000..8a0ea62d --- /dev/null +++ b/src/vs/platform/sharedProcess/node/sharedProcess.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; +import { LogLevel } from 'vs/platform/log/common/log'; + +export interface ISharedProcess { + + /** + * Toggles the visibility of the otherwise hidden + * shared process window. + */ + toggle(): Promise; +} + +export interface ISharedProcessConfiguration { + readonly machineId: string; + readonly windowId: number; + + readonly appRoot: string; + + readonly userEnv: NodeJS.ProcessEnv; + + readonly sharedIPCHandle: string; + + readonly args: NativeParsedArgs; + + readonly logLevel: LogLevel; + + readonly nodeCachedDataDir?: string; + readonly backupWorkspacesPath: string; +} diff --git a/src/vs/platform/state/node/state.ts b/src/vs/platform/state/node/state.ts index b07335f9..9fd896f9 100644 --- a/src/vs/platform/state/node/state.ts +++ b/src/vs/platform/state/node/state.ts @@ -12,6 +12,8 @@ export interface IStateService { getItem(key: string, defaultValue: T): T; getItem(key: string, defaultValue?: T): T | undefined; + setItem(key: string, data?: object | string | number | boolean | undefined | null): void; + removeItem(key: string): void; } diff --git a/src/vs/platform/state/node/stateService.ts b/src/vs/platform/state/node/stateService.ts index 0def1cf9..cb46b77f 100644 --- a/src/vs/platform/state/node/stateService.ts +++ b/src/vs/platform/state/node/stateService.ts @@ -143,7 +143,7 @@ export class StateService implements IStateService { } getItem(key: string, defaultValue: T): T; - getItem(key: string, defaultValue: T | undefined): T | undefined; + getItem(key: string, defaultValue?: T): T | undefined; getItem(key: string, defaultValue?: T): T | undefined { return this.fileStorage.getItem(key, defaultValue); } diff --git a/src/vs/platform/state/test/node/state.test.ts b/src/vs/platform/state/test/node/state.test.ts index 82166e20..dc4acc6a 100644 --- a/src/vs/platform/state/test/node/state.test.ts +++ b/src/vs/platform/state/test/node/state.test.ts @@ -4,31 +4,37 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as os from 'os'; -import * as path from 'vs/base/common/path'; -import { getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { tmpdir } from 'os'; +import { join } from 'vs/base/common/path'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { FileStorage } from 'vs/platform/state/node/stateService'; -import { mkdirp, rimraf, RimRafMode, writeFileSync } from 'vs/base/node/pfs'; +import { mkdirp, rimraf, writeFileSync } from 'vs/base/node/pfs'; -suite('StateService', () => { - const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'stateservice'); - const storageFile = path.join(parentDir, 'storage.json'); +flakySuite('StateService', () => { - teardown(async () => { - await rimraf(parentDir, RimRafMode.MOVE); + let testDir: string; + + setup(() => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'stateservice'); + + return mkdirp(testDir); }); - test('Basics', async () => { - await mkdirp(parentDir); + teardown(() => { + return rimraf(testDir); + }); + + test('Basics', async function () { + const storageFile = join(testDir, 'storage.json'); writeFileSync(storageFile, ''); let service = new FileStorage(storageFile, () => null); service.setItem('some.key', 'some.value'); - assert.equal(service.getItem('some.key'), 'some.value'); + assert.strictEqual(service.getItem('some.key'), 'some.value'); service.removeItem('some.key'); - assert.equal(service.getItem('some.key', 'some.default'), 'some.default'); + assert.strictEqual(service.getItem('some.key', 'some.default'), 'some.default'); assert.ok(!service.getItem('some.unknonw.key')); @@ -36,15 +42,15 @@ suite('StateService', () => { service = new FileStorage(storageFile, () => null); - assert.equal(service.getItem('some.other.key'), 'some.other.value'); + assert.strictEqual(service.getItem('some.other.key'), 'some.other.value'); service.setItem('some.other.key', 'some.other.value'); - assert.equal(service.getItem('some.other.key'), 'some.other.value'); + assert.strictEqual(service.getItem('some.other.key'), 'some.other.value'); service.setItem('some.undefined.key', undefined); - assert.equal(service.getItem('some.undefined.key', 'some.default'), 'some.default'); + assert.strictEqual(service.getItem('some.undefined.key', 'some.default'), 'some.default'); service.setItem('some.null.key', null); - assert.equal(service.getItem('some.null.key', 'some.default'), 'some.default'); + assert.strictEqual(service.getItem('some.null.key', 'some.default'), 'some.default'); }); -}); \ No newline at end of file +}); diff --git a/src/vs/platform/storage/node/storageIpc.ts b/src/vs/platform/storage/node/storageIpc.ts index 8645255d..67195995 100644 --- a/src/vs/platform/storage/node/storageIpc.ts +++ b/src/vs/platform/storage/node/storageIpc.ts @@ -200,12 +200,10 @@ export class GlobalStorageDatabaseChannelClient extends Disposable implements IS return this.channel.call('updateItems', serializableRequest); } - close(): Promise { + async close(): Promise { // when we are about to close, we start to ignore main-side changes since we close anyway dispose(this.onDidChangeItemsOnMainListener); - - return Promise.resolve(); // global storage is closed on the main side } dispose(): void { diff --git a/src/vs/platform/storage/node/storageService.ts b/src/vs/platform/storage/node/storageService.ts index 97cb9b16..5be6557b 100644 --- a/src/vs/platform/storage/node/storageService.ts +++ b/src/vs/platform/storage/node/storageService.ts @@ -12,7 +12,7 @@ import { mark } from 'vs/base/common/performance'; import { join } from 'vs/base/common/path'; import { copy, exists, mkdirp, writeFile } from 'vs/base/node/pfs'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IWorkspaceInitializationPayload, isWorkspaceIdentifier, isSingleFolderWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; +import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; import { assertIsDefined } from 'vs/base/common/types'; import { RunOnceScheduler, runWhenIdle } from 'vs/base/common/async'; @@ -83,7 +83,7 @@ export class NativeStorageService extends AbstractStorageService { const useInMemoryStorage = !!this.environmentService.extensionTestsLocationURI; // no storage during extension tests! // Create workspace storage and initialize - mark('willInitWorkspaceStorage'); + mark('code/willInitWorkspaceStorage'); try { const workspaceStorage = this.createWorkspaceStorage( useInMemoryStorage ? SQLiteStorageDatabase.IN_MEMORY_PATH : join(result.path, NativeStorageService.WORKSPACE_STORAGE_NAME), @@ -99,7 +99,7 @@ export class NativeStorageService extends AbstractStorageService { workspaceStorage.set(IS_NEW_KEY, false); } } finally { - mark('didInitWorkspaceStorage'); + mark('code/didInitWorkspaceStorage'); } } catch (error) { this.logService.error(`[storage] initializeWorkspaceStorage(): Unable to init workspace storage due to ${error}`); @@ -148,23 +148,22 @@ export class NativeStorageService extends AbstractStorageService { private ensureWorkspaceStorageFolderMeta(payload: IWorkspaceInitializationPayload): void { let meta: object | undefined = undefined; - if (isSingleFolderWorkspaceInitializationPayload(payload)) { - meta = { folder: payload.folder.toString() }; + if (isSingleFolderWorkspaceIdentifier(payload)) { + meta = { folder: payload.uri.toString() }; } else if (isWorkspaceIdentifier(payload)) { - meta = { configuration: payload.configPath }; + meta = { workspace: payload.configPath.toString() }; } if (meta) { - const logService = this.logService; - const workspaceStorageMetaPath = join(this.getWorkspaceStorageFolderPath(payload), NativeStorageService.WORKSPACE_META_NAME); - (async function () { + (async () => { try { + const workspaceStorageMetaPath = join(this.getWorkspaceStorageFolderPath(payload), NativeStorageService.WORKSPACE_META_NAME); const storageExists = await exists(workspaceStorageMetaPath); if (!storageExists) { await writeFile(workspaceStorageMetaPath, JSON.stringify(meta, undefined, 2)); } } catch (error) { - logService.error(error); + this.logService.error(error); } })(); } diff --git a/src/vs/platform/storage/test/common/storageService.test.ts b/src/vs/platform/storage/test/common/storageService.test.ts index 886efb2f..182ff12a 100644 --- a/src/vs/platform/storage/test/common/storageService.test.ts +++ b/src/vs/platform/storage/test/common/storageService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { strictEqual, ok, equal } from 'assert'; +import { strictEqual, ok } from 'assert'; import { StorageScope, InMemoryStorageService, StorageTarget, IStorageValueChangeEvent, IStorageTargetChangeEvent } from 'vs/platform/storage/common/storage'; suite('StorageService', function () { @@ -32,15 +32,15 @@ suite('StorageService', function () { storage.store('test.get', 'foobar', scope, StorageTarget.MACHINE); strictEqual(storage.get('test.get', scope, (undefined)!), 'foobar'); let storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.get'); - equal(storageValueChangeEvent?.scope, scope); - equal(storageValueChangeEvent?.key, 'test.get'); + strictEqual(storageValueChangeEvent?.scope, scope); + strictEqual(storageValueChangeEvent?.key, 'test.get'); storageValueChangeEvents = []; storage.store('test.get', '', scope, StorageTarget.MACHINE); strictEqual(storage.get('test.get', scope, (undefined)!), ''); storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.get'); - equal(storageValueChangeEvent!.scope, scope); - equal(storageValueChangeEvent!.key, 'test.get'); + strictEqual(storageValueChangeEvent!.scope, scope); + strictEqual(storageValueChangeEvent!.key, 'test.get'); storage.store('test.getNumber', 5, scope, StorageTarget.MACHINE); strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 5); @@ -79,8 +79,8 @@ suite('StorageService', function () { storage.remove('test.remove', scope); ok(!storage.get('test.remove', scope, (undefined)!)); let storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.remove'); - equal(storageValueChangeEvent?.scope, scope); - equal(storageValueChangeEvent?.key, 'test.remove'); + strictEqual(storageValueChangeEvent?.scope, scope); + strictEqual(storageValueChangeEvent?.key, 'test.remove'); } test('Keys (in-memory)', () => { @@ -107,20 +107,20 @@ suite('StorageService', function () { storage.store('test.target1', 'value1', scope, target); strictEqual(storage.keys(scope, target).length, 1); - equal(storageTargetEvent?.scope, scope); - equal(storageValueChangeEvent?.key, 'test.target1'); - equal(storageValueChangeEvent?.scope, scope); - equal(storageValueChangeEvent?.target, target); + strictEqual(storageTargetEvent?.scope, scope); + strictEqual(storageValueChangeEvent?.key, 'test.target1'); + strictEqual(storageValueChangeEvent?.scope, scope); + strictEqual(storageValueChangeEvent?.target, target); storageTargetEvent = undefined; storageValueChangeEvent = Object.create(null); storage.store('test.target1', 'otherValue1', scope, target); strictEqual(storage.keys(scope, target).length, 1); - equal(storageTargetEvent, undefined); - equal(storageValueChangeEvent?.key, 'test.target1'); - equal(storageValueChangeEvent?.scope, scope); - equal(storageValueChangeEvent?.target, target); + strictEqual(storageTargetEvent, undefined); + strictEqual(storageValueChangeEvent?.key, 'test.target1'); + strictEqual(storageValueChangeEvent?.scope, scope); + strictEqual(storageValueChangeEvent?.target, target); storage.store('test.target2', 'value2', scope, target); storage.store('test.target3', 'value3', scope, target); @@ -142,9 +142,9 @@ suite('StorageService', function () { storage.remove('test.target4', scope); strictEqual(storage.keys(scope, target).length, keysLength); - equal(storageTargetEvent?.scope, scope); - equal(storageValueChangeEvent?.key, 'test.target4'); - equal(storageValueChangeEvent?.scope, scope); + strictEqual(storageTargetEvent?.scope, scope); + strictEqual(storageValueChangeEvent?.key, 'test.target4'); + strictEqual(storageValueChangeEvent?.scope, scope); } } @@ -171,7 +171,7 @@ suite('StorageService', function () { storage.store('test.target1', undefined, scope, target); strictEqual(storage.keys(scope, target).length, 0); - equal(storageTargetEvent?.scope, scope); + strictEqual(storageTargetEvent?.scope, scope); storage.store('test.target1', '', scope, target); strictEqual(storage.keys(scope, target).length, 1); diff --git a/src/vs/platform/storage/test/electron-browser/storage.test.ts b/src/vs/platform/storage/test/electron-browser/storage.test.ts index 4e5e10a4..56f0a638 100644 --- a/src/vs/platform/storage/test/electron-browser/storage.test.ts +++ b/src/vs/platform/storage/test/electron-browser/storage.test.ts @@ -3,12 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { equal } from 'assert'; +import { strictEqual } from 'assert'; import { FileStorageDatabase } from 'vs/platform/storage/browser/storageService'; -import { generateUuid } from 'vs/base/common/uuid'; import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { rimraf, RimRafMode } from 'vs/base/node/pfs'; +import { rimraf } from 'vs/base/node/pfs'; import { NullLogService } from 'vs/platform/log/common/log'; import { Storage } from 'vs/base/parts/storage/common/storage'; import { URI } from 'vs/base/common/uri'; @@ -20,11 +19,10 @@ import { Schemas } from 'vs/base/common/network'; suite('Storage', () => { - const parentDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice'); + let testDir: string; let fileService: FileService; let fileProvider: DiskFileSystemProvider; - let testDir: string; const disposables = new DisposableStore(); @@ -38,14 +36,13 @@ suite('Storage', () => { disposables.add(fileService.registerProvider(Schemas.file, fileProvider)); disposables.add(fileProvider); - const id = generateUuid(); - testDir = join(parentDir, id); + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice'); }); - teardown(async () => { + teardown(() => { disposables.clear(); - await rimraf(parentDir, RimRafMode.MOVE); + return rimraf(testDir); }); test('File Based Storage', async () => { @@ -57,9 +54,9 @@ suite('Storage', () => { storage.set('barNumber', 55); storage.set('barBoolean', true); - equal(storage.get('bar'), 'foo'); - equal(storage.get('barNumber'), '55'); - equal(storage.get('barBoolean'), 'true'); + strictEqual(storage.get('bar'), 'foo'); + strictEqual(storage.get('barNumber'), '55'); + strictEqual(storage.get('barBoolean'), 'true'); await storage.close(); @@ -67,17 +64,17 @@ suite('Storage', () => { await storage.init(); - equal(storage.get('bar'), 'foo'); - equal(storage.get('barNumber'), '55'); - equal(storage.get('barBoolean'), 'true'); + strictEqual(storage.get('bar'), 'foo'); + strictEqual(storage.get('barNumber'), '55'); + strictEqual(storage.get('barBoolean'), 'true'); storage.delete('bar'); storage.delete('barNumber'); storage.delete('barBoolean'); - equal(storage.get('bar', 'undefined'), 'undefined'); - equal(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber'); - equal(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean'); + strictEqual(storage.get('bar', 'undefined'), 'undefined'); + strictEqual(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber'); + strictEqual(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean'); await storage.close(); @@ -85,8 +82,8 @@ suite('Storage', () => { await storage.init(); - equal(storage.get('bar', 'undefined'), 'undefined'); - equal(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber'); - equal(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean'); + strictEqual(storage.get('bar', 'undefined'), 'undefined'); + strictEqual(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber'); + strictEqual(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean'); }); }); diff --git a/src/vs/platform/storage/test/node/storageService.test.ts b/src/vs/platform/storage/test/node/storageService.test.ts index e0a410ef..6677ab24 100644 --- a/src/vs/platform/storage/test/node/storageService.test.ts +++ b/src/vs/platform/storage/test/node/storageService.test.ts @@ -3,33 +3,33 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { equal } from 'assert'; +import { strictEqual } from 'assert'; import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { NativeStorageService } from 'vs/platform/storage/node/storageService'; -import { generateUuid } from 'vs/base/common/uuid'; -import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { mkdirp, rimraf, RimRafMode } from 'vs/base/node/pfs'; +import { mkdirp, rimraf } from 'vs/base/node/pfs'; import { NullLogService } from 'vs/platform/log/common/log'; import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; import { InMemoryStorageDatabase } from 'vs/base/parts/storage/common/storage'; import { URI } from 'vs/base/common/uri'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; -suite('NativeStorageService', function () { +flakySuite('NativeStorageService', function () { - function uniqueStorageDir(): string { - const id = generateUuid(); + let testDir: string; - return join(tmpdir(), 'vsctests', id, 'storage2', id); - } + setup(() => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice'); - test('Migrate Data', async () => { + return mkdirp(testDir); + }); - // Given issues such as https://github.com/microsoft/vscode/issues/108113 - // we see random test failures when accessing the native file system. - this.retries(3); - this.timeout(1000 * 20); + teardown(() => { + return rimraf(testDir); + }); + + test('Migrate Data', async function () { class StorageTestEnvironmentService extends NativeEnvironmentService { @@ -46,27 +46,23 @@ suite('NativeStorageService', function () { } } - const storageDir = uniqueStorageDir(); - await mkdirp(storageDir); - - const storage = new NativeStorageService(new InMemoryStorageDatabase(), new NullLogService(), new StorageTestEnvironmentService(URI.file(storageDir), storageDir)); + const storage = new NativeStorageService(new InMemoryStorageDatabase(), new NullLogService(), new StorageTestEnvironmentService(URI.file(testDir), testDir)); await storage.initialize({ id: String(Date.now()) }); storage.store('bar', 'foo', StorageScope.WORKSPACE, StorageTarget.MACHINE); storage.store('barNumber', 55, StorageScope.WORKSPACE, StorageTarget.MACHINE); storage.store('barBoolean', true, StorageScope.GLOBAL, StorageTarget.MACHINE); - equal(storage.get('bar', StorageScope.WORKSPACE), 'foo'); - equal(storage.getNumber('barNumber', StorageScope.WORKSPACE), 55); - equal(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true); + strictEqual(storage.get('bar', StorageScope.WORKSPACE), 'foo'); + strictEqual(storage.getNumber('barNumber', StorageScope.WORKSPACE), 55); + strictEqual(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true); await storage.migrate({ id: String(Date.now() + 100) }); - equal(storage.get('bar', StorageScope.WORKSPACE), 'foo'); - equal(storage.getNumber('barNumber', StorageScope.WORKSPACE), 55); - equal(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true); + strictEqual(storage.get('bar', StorageScope.WORKSPACE), 'foo'); + strictEqual(storage.getNumber('barNumber', StorageScope.WORKSPACE), 55); + strictEqual(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true); await storage.close(); - await rimraf(storageDir, RimRafMode.MOVE); }); }); diff --git a/src/vs/platform/telemetry/node/commonProperties.ts b/src/vs/platform/telemetry/common/commonProperties.ts similarity index 78% rename from src/vs/platform/telemetry/node/commonProperties.ts rename to src/vs/platform/telemetry/common/commonProperties.ts index d681c0c7..46f08b5d 100644 --- a/src/vs/platform/telemetry/node/commonProperties.ts +++ b/src/vs/platform/telemetry/common/commonProperties.ts @@ -3,12 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as Platform from 'vs/base/common/platform'; -import * as os from 'os'; -import * as uuid from 'vs/base/common/uuid'; -import { readFile } from 'vs/base/node/pfs'; +import { IFileService } from 'vs/platform/files/common/files'; +import { isLinuxSnap, PlatformToString, platform } from 'vs/base/common/platform'; +import { platform as nodePlatform, env } from 'vs/base/common/process'; +import { generateUuid } from 'vs/base/common/uuid'; +import { URI } from 'vs/base/common/uri'; export async function resolveCommonProperties( + fileService: IFileService, + release: string, + arch: string, commit: string | undefined, version: string | undefined, machineId: string | undefined, @@ -21,19 +25,19 @@ export async function resolveCommonProperties( // __GDPR__COMMON__ "common.machineId" : { "endPoint": "MacAddressHash", "classification": "EndUserPseudonymizedInformation", "purpose": "FeatureInsight" } result['common.machineId'] = machineId; // __GDPR__COMMON__ "sessionID" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - result['sessionID'] = uuid.generateUuid() + Date.now(); + result['sessionID'] = generateUuid() + Date.now(); // __GDPR__COMMON__ "commitHash" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } result['commitHash'] = commit; // __GDPR__COMMON__ "version" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } result['version'] = version; // __GDPR__COMMON__ "common.platformVersion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - result['common.platformVersion'] = (os.release() || '').replace(/^(\d+)(\.\d+)?(\.\d+)?(.*)/, '$1$2$3'); + result['common.platformVersion'] = (release || '').replace(/^(\d+)(\.\d+)?(\.\d+)?(.*)/, '$1$2$3'); // __GDPR__COMMON__ "common.platform" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - result['common.platform'] = Platform.PlatformToString(Platform.platform); + result['common.platform'] = PlatformToString(platform); // __GDPR__COMMON__ "common.nodePlatform" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - result['common.nodePlatform'] = process.platform; + result['common.nodePlatform'] = nodePlatform; // __GDPR__COMMON__ "common.nodeArch" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - result['common.nodeArch'] = process.arch; + result['common.nodeArch'] = arch; // __GDPR__COMMON__ "common.product" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } result['common.product'] = product || 'desktop'; @@ -64,16 +68,16 @@ export async function resolveCommonProperties( } }); - if (process.platform === 'linux' && process.env.SNAP && process.env.SNAP_REVISION) { + if (isLinuxSnap) { // __GDPR__COMMON__ "common.snap" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } result['common.snap'] = 'true'; } try { - const contents = await readFile(installSourcePath, 'utf8'); + const contents = await fileService.readFile(URI.file(installSourcePath)); // __GDPR__COMMON__ "common.source" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - result['common.source'] = contents.slice(0, 30); + result['common.source'] = contents.value.toString().slice(0, 30); } catch (error) { // ignore error } @@ -82,10 +86,11 @@ export async function resolveCommonProperties( } function verifyMicrosoftInternalDomain(domainList: readonly string[]): boolean { - if (!process || !process.env || !process.env['USERDNSDOMAIN']) { + const userDnsDomain = env['USERDNSDOMAIN']; + if (!userDnsDomain) { return false; } - const domain = process.env['USERDNSDOMAIN']!.toLowerCase(); + const domain = userDnsDomain.toLowerCase(); return domainList.some(msftDomain => domain === msftDomain); } diff --git a/src/vs/platform/telemetry/node/telemetryIpc.ts b/src/vs/platform/telemetry/common/telemetryIpc.ts similarity index 100% rename from src/vs/platform/telemetry/node/telemetryIpc.ts rename to src/vs/platform/telemetry/common/telemetryIpc.ts diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index b5239353..7bf7747c 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -332,6 +332,12 @@ export const editorHoverStatusBarBackground = registerColor('editorHoverWidget.s */ export const editorActiveLinkForeground = registerColor('editorLink.activeForeground', { dark: '#4E94CE', light: Color.blue, hc: Color.cyan }, nls.localize('activeLinkForeground', 'Color of active links.')); +/** + * Inline hints + */ +export const editorInlineHintForeground = registerColor('editorInlineHint.foreground', { dark: editorWidgetBackground, light: editorWidgetForeground, hc: editorWidgetBackground }, nls.localize('editorInlineHintForeground', 'Foreground color of inline hints')); +export const editorInlineHintBackground = registerColor('editorInlineHint.background', { dark: editorWidgetForeground, light: editorWidgetBackground, hc: editorWidgetForeground }, nls.localize('editorInlineHintBackground', 'Background color of inline hints')); + /** * Editor lighbulb icon colors */ diff --git a/src/vs/platform/theme/common/iconRegistry.ts b/src/vs/platform/theme/common/iconRegistry.ts index a19a41a8..4079cc4e 100644 --- a/src/vs/platform/theme/common/iconRegistry.ts +++ b/src/vs/platform/theme/common/iconRegistry.ts @@ -12,7 +12,6 @@ import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/plat import { RunOnceScheduler } from 'vs/base/common/async'; import * as Codicons from 'vs/base/common/codicons'; - // ------ API types @@ -190,11 +189,6 @@ class IconRegistry implements IIconRegistry { public toString() { const sorter = (i1: IconContribution, i2: IconContribution) => { - const isThemeIcon1 = ThemeIcon.isThemeIcon(i1.defaults); - const isThemeIcon2 = ThemeIcon.isThemeIcon(i2.defaults); - if (isThemeIcon1 !== isThemeIcon2) { - return isThemeIcon1 ? -1 : 1; - } return i1.id.localeCompare(i2.id); }; const classNames = (i: IconContribution) => { @@ -205,18 +199,24 @@ class IconRegistry implements IIconRegistry { }; let reference = []; - let docCss = []; + reference.push(`| preview | identifier | default codicon id | description`); + reference.push(`| ----------- | --------------------------------- | --------------------------------- | --------------------------------- |`); const contributions = Object.keys(this.iconsById).map(key => this.iconsById[key]); - for (const i of contributions.sort(sorter)) { - reference.push(`||${i.id}|${ThemeIcon.isThemeIcon(i.defaults) ? i.defaults.id : ''}|${i.description || ''}|`); - - if (!ThemeIcon.isThemeIcon((i.defaults))) { - docCss.push(`.codicon-${i.id}:before { content: "${i.defaults.character}" }`); - } + for (const i of contributions.filter(i => !!i.description).sort(sorter)) { + reference.push(`||${i.id}|${ThemeIcon.isThemeIcon(i.defaults) ? i.defaults.id : i.id}|${i.description || ''}|`); } - return reference.join('\n') + '\n\n' + docCss.join('\n'); + + reference.push(`| preview | identifier `); + reference.push(`| ----------- | --------------------------------- |`); + + for (const i of contributions.filter(i => !ThemeIcon.isThemeIcon(i.defaults)).sort(sorter)) { + reference.push(`||${i.id}|`); + + } + + return reference.join('\n'); } } @@ -262,3 +262,5 @@ export const widgetClose = registerIcon('widget-close', Codicons.Codicon.close, export const gotoPreviousLocation = registerIcon('goto-previous-location', Codicons.Codicon.arrowUp, localize('previousChangeIcon', 'Icon for goto previous editor location.')); export const gotoNextLocation = registerIcon('goto-next-location', Codicons.Codicon.arrowDown, localize('nextChangeIcon', 'Icon for goto next editor location.')); + +export const syncing = ThemeIcon.modify(Codicons.Codicon.sync, 'spin'); diff --git a/src/vs/platform/theme/common/themeService.ts b/src/vs/platform/theme/common/themeService.ts index f82c4576..1096c6be 100644 --- a/src/vs/platform/theme/common/themeService.ts +++ b/src/vs/platform/theme/common/themeService.ts @@ -11,7 +11,7 @@ import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry'; import { Event, Emitter } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ColorScheme } from 'vs/platform/theme/common/theme'; -import { CSSIcon } from 'vs/base/common/codicons'; +import { Codicon, CSSIcon } from 'vs/base/common/codicons'; export const IThemeService = createDecorator('themeService'); @@ -70,50 +70,13 @@ export namespace ThemeIcon { return ti1.id === ti2.id && ti1.color?.id === ti2.color?.id; } - const _regexAsClassName = /^(codicon\/)?([a-z-]+)(~[a-z]+)?$/i; - - export function asClassNameArray(icon: ThemeIcon): string[] { - const match = _regexAsClassName.exec(icon.id); - if (!match) { - return ['codicon', 'codicon-error']; - } - let [, , name, modifier] = match; - let className = `codicon-${name}`; - if (modifier) { - return ['codicon', className, modifier.substr(1)]; - } - return ['codicon', className]; - } - - - export function asClassName(icon: ThemeIcon): string { - return asClassNameArray(icon).join(' '); - } - - export function asCSSSelector(icon: ThemeIcon): string { - return '.' + asClassNameArray(icon).join('.'); - } - - export function asCSSIcon(icon: ThemeIcon): CSSIcon { - return { - classNames: asClassName(icon) - }; - } - - export function asCodiconLabel(icon: ThemeIcon): string { - return '$(' + icon.id + ')'; - } - - export function revive(icon: any): ThemeIcon | undefined { - if (ThemeIcon.isThemeIcon(icon)) { - return { id: icon.id, color: icon.color ? { id: icon.color.id } : undefined }; - } - return undefined; - } + export const asClassNameArray: (icon: ThemeIcon) => string[] = CSSIcon.asClassNameArray; + export const asClassName: (icon: ThemeIcon) => string = CSSIcon.asClassName; + export const asCSSSelector: (icon: ThemeIcon) => string = CSSIcon.asCSSSelector; } -export const FileThemeIcon = { id: 'file' }; -export const FolderThemeIcon = { id: 'folder' }; +export const FileThemeIcon = Codicon.file; +export const FolderThemeIcon = Codicon.folder; export function getThemeTypeSelector(type: ColorScheme): string { switch (type) { diff --git a/src/vs/platform/theme/test/common/testThemeService.ts b/src/vs/platform/theme/test/common/testThemeService.ts index 2bdd34d8..e21487ef 100644 --- a/src/vs/platform/theme/test/common/testThemeService.ts +++ b/src/vs/platform/theme/test/common/testThemeService.ts @@ -12,8 +12,11 @@ export class TestColorTheme implements IColorTheme { public readonly label = 'test'; - constructor(private colors: { [id: string]: string; } = {}, public type = ColorScheme.DARK) { - } + constructor( + private colors: { [id: string]: string; } = {}, + public type = ColorScheme.DARK, + public readonly semanticHighlighting = false + ) { } getColor(color: string, useDefault?: boolean): Color | undefined { let value = this.colors[color]; @@ -31,8 +34,6 @@ export class TestColorTheme implements IColorTheme { return undefined; } - readonly semanticHighlighting = false; - get tokenColorMap(): string[] { return []; } diff --git a/src/vs/platform/undoRedo/common/undoRedo.ts b/src/vs/platform/undoRedo/common/undoRedo.ts index 021003ba..80512775 100644 --- a/src/vs/platform/undoRedo/common/undoRedo.ts +++ b/src/vs/platform/undoRedo/common/undoRedo.ts @@ -18,6 +18,10 @@ export interface IResourceUndoRedoElement { readonly type: UndoRedoElementType.Resource; readonly resource: URI; readonly label: string; + /** + * Show a message to the user confirming when trying to undo this element + */ + readonly confirmBeforeUndo?: boolean; undo(): Promise | void; redo(): Promise | void; } @@ -26,6 +30,10 @@ export interface IWorkspaceUndoRedoElement { readonly type: UndoRedoElementType.Workspace; readonly resources: readonly URI[]; readonly label: string; + /** + * Show a message to the user confirming when trying to undo this element + */ + readonly confirmBeforeUndo?: boolean; undo(): Promise | void; redo(): Promise | void; diff --git a/src/vs/platform/undoRedo/common/undoRedoService.ts b/src/vs/platform/undoRedo/common/undoRedoService.ts index 2fb802fb..7e205e70 100644 --- a/src/vs/platform/undoRedo/common/undoRedoService.ts +++ b/src/vs/platform/undoRedo/common/undoRedoService.ts @@ -27,6 +27,7 @@ class ResourceStackElement { public readonly type = UndoRedoElementType.Resource; public readonly actual: IUndoRedoElement; public readonly label: string; + public readonly confirmBeforeUndo: boolean; public readonly resourceLabel: string; public readonly strResource: string; @@ -41,6 +42,7 @@ class ResourceStackElement { constructor(actual: IUndoRedoElement, resourceLabel: string, strResource: string, groupId: number, groupOrder: number, sourceId: number, sourceOrder: number) { this.actual = actual; this.label = actual.label; + this.confirmBeforeUndo = actual.confirmBeforeUndo || false; this.resourceLabel = resourceLabel; this.strResource = strResource; this.resourceLabels = [this.resourceLabel]; @@ -129,6 +131,7 @@ class WorkspaceStackElement { public readonly type = UndoRedoElementType.Workspace; public readonly actual: IWorkspaceUndoRedoElement; public readonly label: string; + public readonly confirmBeforeUndo: boolean; public readonly resourceLabels: string[]; public readonly strResources: string[]; @@ -142,6 +145,7 @@ class WorkspaceStackElement { constructor(actual: IWorkspaceUndoRedoElement, resourceLabels: string[], strResources: string[], groupId: number, groupOrder: number, sourceId: number, sourceOrder: number) { this.actual = actual; this.label = actual.label; + this.confirmBeforeUndo = actual.confirmBeforeUndo || false; this.resourceLabels = resourceLabels; this.strResources = strResources; this.groupId = groupId; @@ -811,7 +815,7 @@ export class UndoRedoService implements IUndoRedoService { if (element.canSplit()) { this._splitPastWorkspaceElement(element, ignoreResources); this._notificationService.info(message); - return new WorkspaceVerificationError(this._undo(strResource)); + return new WorkspaceVerificationError(this._undo(strResource, 0, true)); } else { // Cannot safely split this workspace element => flush all undo/redo stacks for (const strResource of element.strResources) { @@ -899,13 +903,13 @@ export class UndoRedoService implements IUndoRedoService { return null; } - private _workspaceUndo(strResource: string, element: WorkspaceStackElement): Promise | void { + private _workspaceUndo(strResource: string, element: WorkspaceStackElement, undoConfirmed: boolean): Promise | void { const affectedEditStacks = this._getAffectedEditStacks(element); const verificationError = this._checkWorkspaceUndo(strResource, element, affectedEditStacks, /*invalidated resources will be checked after the prepare call*/false); if (verificationError) { return verificationError.returnValue; } - return this._confirmAndExecuteWorkspaceUndo(strResource, element, affectedEditStacks); + return this._confirmAndExecuteWorkspaceUndo(strResource, element, affectedEditStacks, undoConfirmed); } private _isPartOfUndoGroup(element: WorkspaceStackElement): boolean { @@ -933,7 +937,7 @@ export class UndoRedoService implements IUndoRedoService { return false; } - private async _confirmAndExecuteWorkspaceUndo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot): Promise { + private async _confirmAndExecuteWorkspaceUndo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot, undoConfirmed: boolean): Promise { if (element.canSplit() && !this._isPartOfUndoGroup(element)) { // this element can be split @@ -959,7 +963,7 @@ export class UndoRedoService implements IUndoRedoService { if (result.choice === 1) { // choice: undo this file this._splitPastWorkspaceElement(element, null); - return this._undo(strResource); + return this._undo(strResource, 0, true); } // choice: undo in all files @@ -969,6 +973,8 @@ export class UndoRedoService implements IUndoRedoService { if (verificationError1) { return verificationError1.returnValue; } + + undoConfirmed = true; } // prepare @@ -989,10 +995,10 @@ export class UndoRedoService implements IUndoRedoService { for (const editStack of editStackSnapshot.editStacks) { editStack.moveBackward(element); } - return this._safeInvokeWithLocks(element, () => element.actual.undo(), editStackSnapshot, cleanup, () => this._continueUndoInGroup(element.groupId)); + return this._safeInvokeWithLocks(element, () => element.actual.undo(), editStackSnapshot, cleanup, () => this._continueUndoInGroup(element.groupId, undoConfirmed)); } - private _resourceUndo(editStack: ResourceEditStack, element: ResourceStackElement): Promise | void { + private _resourceUndo(editStack: ResourceEditStack, element: ResourceStackElement, undoConfirmed: boolean): Promise | void { if (!element.isValid) { // invalid element => immediately flush edit stack! editStack.flushAllElements(); @@ -1008,7 +1014,7 @@ export class UndoRedoService implements IUndoRedoService { } return this._invokeResourcePrepare(element, (cleanup) => { editStack.moveBackward(element); - return this._safeInvokeWithLocks(element, () => element.actual.undo(), new EditStackSnapshot([editStack]), cleanup, () => this._continueUndoInGroup(element.groupId)); + return this._safeInvokeWithLocks(element, () => element.actual.undo(), new EditStackSnapshot([editStack]), cleanup, () => this._continueUndoInGroup(element.groupId, undoConfirmed)); }); } @@ -1037,29 +1043,29 @@ export class UndoRedoService implements IUndoRedoService { return [matchedElement, matchedStrResource]; } - private _continueUndoInGroup(groupId: number): Promise | void { + private _continueUndoInGroup(groupId: number, undoConfirmed: boolean): Promise | void { if (!groupId) { return; } const [, matchedStrResource] = this._findClosestUndoElementInGroup(groupId); if (matchedStrResource) { - return this._undo(matchedStrResource); + return this._undo(matchedStrResource, 0, undoConfirmed); } } public undo(resourceOrSource: URI | UndoRedoSource): Promise | void { if (resourceOrSource instanceof UndoRedoSource) { const [, matchedStrResource] = this._findClosestUndoElementWithSource(resourceOrSource.id); - return matchedStrResource ? this._undo(matchedStrResource, resourceOrSource.id) : undefined; + return matchedStrResource ? this._undo(matchedStrResource, resourceOrSource.id, false) : undefined; } if (typeof resourceOrSource === 'string') { - return this._undo(resourceOrSource); + return this._undo(resourceOrSource, 0, false); } - return this._undo(this.getUriComparisonKey(resourceOrSource)); + return this._undo(this.getUriComparisonKey(resourceOrSource), 0, false); } - private _undo(strResource: string, sourceId: number = 0): Promise | void { + private _undo(strResource: string, sourceId: number = 0, undoConfirmed: boolean): Promise | void { if (!this._editStacks.has(strResource)) { return; } @@ -1075,20 +1081,21 @@ export class UndoRedoService implements IUndoRedoService { const [matchedElement, matchedStrResource] = this._findClosestUndoElementInGroup(element.groupId); if (element !== matchedElement && matchedStrResource) { // there is an element in the same group that should be undone before this one - return this._undo(matchedStrResource); + return this._undo(matchedStrResource, sourceId, undoConfirmed); } } - if (element.sourceId !== sourceId) { - // Hit a different source, prompt for confirmation - return this._confirmDifferentSourceAndContinueUndo(strResource, element); + const shouldPromptForConfirmation = (element.sourceId !== sourceId || element.confirmBeforeUndo); + if (shouldPromptForConfirmation && !undoConfirmed) { + // Hit a different source or the element asks for prompt before undo, prompt for confirmation + return this._confirmAndContinueUndo(strResource, sourceId, element); } try { if (element.type === UndoRedoElementType.Workspace) { - return this._workspaceUndo(strResource, element); + return this._workspaceUndo(strResource, element, undoConfirmed); } else { - return this._resourceUndo(editStack, element); + return this._resourceUndo(editStack, element, undoConfirmed); } } finally { if (DEBUG) { @@ -1097,7 +1104,7 @@ export class UndoRedoService implements IUndoRedoService { } } - private async _confirmDifferentSourceAndContinueUndo(strResource: string, element: StackElement): Promise { + private async _confirmAndContinueUndo(strResource: string, sourceId: number, element: StackElement): Promise { const result = await this._dialogService.show( Severity.Info, nls.localize('confirmDifferentSource', "Would you like to undo '{0}'?", element.label), @@ -1116,7 +1123,7 @@ export class UndoRedoService implements IUndoRedoService { } // choice: undo - return this._undo(strResource, element.sourceId); + return this._undo(strResource, sourceId, true); } private _findClosestRedoElementWithSource(sourceId: number): [StackElement | null, string | null] { diff --git a/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts b/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts index 4c0a48e5..49f49445 100644 --- a/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts +++ b/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts @@ -23,9 +23,9 @@ suite('UndoRedoService', () => { const resource = URI.file('test.txt'); const service = createUndoRedoService(); - assert.equal(service.canUndo(resource), false); - assert.equal(service.canRedo(resource), false); - assert.equal(service.hasElements(resource), false); + assert.strictEqual(service.canUndo(resource), false); + assert.strictEqual(service.canRedo(resource), false); + assert.strictEqual(service.hasElements(resource), false); assert.ok(service.getLastElement(resource) === null); let undoCall1 = 0; @@ -39,27 +39,27 @@ suite('UndoRedoService', () => { }; service.pushElement(element1); - assert.equal(undoCall1, 0); - assert.equal(redoCall1, 0); - assert.equal(service.canUndo(resource), true); - assert.equal(service.canRedo(resource), false); - assert.equal(service.hasElements(resource), true); + assert.strictEqual(undoCall1, 0); + assert.strictEqual(redoCall1, 0); + assert.strictEqual(service.canUndo(resource), true); + assert.strictEqual(service.canRedo(resource), false); + assert.strictEqual(service.hasElements(resource), true); assert.ok(service.getLastElement(resource) === element1); service.undo(resource); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 0); - assert.equal(service.canUndo(resource), false); - assert.equal(service.canRedo(resource), true); - assert.equal(service.hasElements(resource), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 0); + assert.strictEqual(service.canUndo(resource), false); + assert.strictEqual(service.canRedo(resource), true); + assert.strictEqual(service.hasElements(resource), true); assert.ok(service.getLastElement(resource) === null); service.redo(resource); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 1); - assert.equal(service.canUndo(resource), true); - assert.equal(service.canRedo(resource), false); - assert.equal(service.hasElements(resource), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 1); + assert.strictEqual(service.canUndo(resource), true); + assert.strictEqual(service.canRedo(resource), false); + assert.strictEqual(service.hasElements(resource), true); assert.ok(service.getLastElement(resource) === element1); let undoCall2 = 0; @@ -73,24 +73,24 @@ suite('UndoRedoService', () => { }; service.pushElement(element2); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 1); - assert.equal(undoCall2, 0); - assert.equal(redoCall2, 0); - assert.equal(service.canUndo(resource), true); - assert.equal(service.canRedo(resource), false); - assert.equal(service.hasElements(resource), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 1); + assert.strictEqual(undoCall2, 0); + assert.strictEqual(redoCall2, 0); + assert.strictEqual(service.canUndo(resource), true); + assert.strictEqual(service.canRedo(resource), false); + assert.strictEqual(service.hasElements(resource), true); assert.ok(service.getLastElement(resource) === element2); service.undo(resource); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 1); - assert.equal(undoCall2, 1); - assert.equal(redoCall2, 0); - assert.equal(service.canUndo(resource), true); - assert.equal(service.canRedo(resource), true); - assert.equal(service.hasElements(resource), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 1); + assert.strictEqual(undoCall2, 1); + assert.strictEqual(redoCall2, 0); + assert.strictEqual(service.canUndo(resource), true); + assert.strictEqual(service.canRedo(resource), true); + assert.strictEqual(service.hasElements(resource), true); assert.ok(service.getLastElement(resource) === null); let undoCall3 = 0; @@ -104,28 +104,28 @@ suite('UndoRedoService', () => { }; service.pushElement(element3); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 1); - assert.equal(undoCall2, 1); - assert.equal(redoCall2, 0); - assert.equal(undoCall3, 0); - assert.equal(redoCall3, 0); - assert.equal(service.canUndo(resource), true); - assert.equal(service.canRedo(resource), false); - assert.equal(service.hasElements(resource), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 1); + assert.strictEqual(undoCall2, 1); + assert.strictEqual(redoCall2, 0); + assert.strictEqual(undoCall3, 0); + assert.strictEqual(redoCall3, 0); + assert.strictEqual(service.canUndo(resource), true); + assert.strictEqual(service.canRedo(resource), false); + assert.strictEqual(service.hasElements(resource), true); assert.ok(service.getLastElement(resource) === element3); service.undo(resource); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 1); - assert.equal(undoCall2, 1); - assert.equal(redoCall2, 0); - assert.equal(undoCall3, 1); - assert.equal(redoCall3, 0); - assert.equal(service.canUndo(resource), true); - assert.equal(service.canRedo(resource), true); - assert.equal(service.hasElements(resource), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 1); + assert.strictEqual(undoCall2, 1); + assert.strictEqual(redoCall2, 0); + assert.strictEqual(undoCall3, 1); + assert.strictEqual(redoCall3, 0); + assert.strictEqual(service.canUndo(resource), true); + assert.strictEqual(service.canRedo(resource), true); + assert.strictEqual(service.hasElements(resource), true); assert.ok(service.getLastElement(resource) === null); }); @@ -169,50 +169,50 @@ suite('UndoRedoService', () => { }; service.pushElement(element1); - assert.equal(service.canUndo(resource1), true); - assert.equal(service.canRedo(resource1), false); - assert.equal(service.hasElements(resource1), true); + assert.strictEqual(service.canUndo(resource1), true); + assert.strictEqual(service.canRedo(resource1), false); + assert.strictEqual(service.hasElements(resource1), true); assert.ok(service.getLastElement(resource1) === element1); - assert.equal(service.canUndo(resource2), true); - assert.equal(service.canRedo(resource2), false); - assert.equal(service.hasElements(resource2), true); + assert.strictEqual(service.canUndo(resource2), true); + assert.strictEqual(service.canRedo(resource2), false); + assert.strictEqual(service.hasElements(resource2), true); assert.ok(service.getLastElement(resource2) === element1); await service.undo(resource1); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 0); - assert.equal(service.canUndo(resource1), false); - assert.equal(service.canRedo(resource1), true); - assert.equal(service.hasElements(resource1), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 0); + assert.strictEqual(service.canUndo(resource1), false); + assert.strictEqual(service.canRedo(resource1), true); + assert.strictEqual(service.hasElements(resource1), true); assert.ok(service.getLastElement(resource1) === null); - assert.equal(service.canUndo(resource2), false); - assert.equal(service.canRedo(resource2), true); - assert.equal(service.hasElements(resource2), true); + assert.strictEqual(service.canUndo(resource2), false); + assert.strictEqual(service.canRedo(resource2), true); + assert.strictEqual(service.hasElements(resource2), true); assert.ok(service.getLastElement(resource2) === null); await service.redo(resource2); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 1); - assert.equal(undoCall11, 0); - assert.equal(redoCall11, 0); - assert.equal(undoCall12, 0); - assert.equal(redoCall12, 0); - assert.equal(service.canUndo(resource1), true); - assert.equal(service.canRedo(resource1), false); - assert.equal(service.hasElements(resource1), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 1); + assert.strictEqual(undoCall11, 0); + assert.strictEqual(redoCall11, 0); + assert.strictEqual(undoCall12, 0); + assert.strictEqual(redoCall12, 0); + assert.strictEqual(service.canUndo(resource1), true); + assert.strictEqual(service.canRedo(resource1), false); + assert.strictEqual(service.hasElements(resource1), true); assert.ok(service.getLastElement(resource1) === element1); - assert.equal(service.canUndo(resource2), true); - assert.equal(service.canRedo(resource2), false); - assert.equal(service.hasElements(resource2), true); + assert.strictEqual(service.canUndo(resource2), true); + assert.strictEqual(service.canRedo(resource2), false); + assert.strictEqual(service.hasElements(resource2), true); assert.ok(service.getLastElement(resource2) === element1); }); test('UndoRedoGroup.None uses id 0', () => { - assert.equal(UndoRedoGroup.None.id, 0); - assert.equal(UndoRedoGroup.None.nextOrder(), 0); - assert.equal(UndoRedoGroup.None.nextOrder(), 0); + assert.strictEqual(UndoRedoGroup.None.id, 0); + assert.strictEqual(UndoRedoGroup.None.nextOrder(), 0); + assert.strictEqual(UndoRedoGroup.None.nextOrder(), 0); }); }); diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 2e18cc8c..ddfa1ee4 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -15,6 +15,7 @@ import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/e import { ILogService } from 'vs/platform/log/common/log'; import { AbstractUpdateService, createUpdateURL, UpdateNotAvailableClassification } from 'vs/platform/update/electron-main/abstractUpdateService'; import { IRequestService } from 'vs/platform/request/common/request'; +import product from 'vs/platform/product/common/product'; export class DarwinUpdateService extends AbstractUpdateService { @@ -56,7 +57,12 @@ export class DarwinUpdateService extends AbstractUpdateService { } protected buildUpdateFeedUrl(quality: string): string | undefined { - const assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64'; + let assetID: string; + if (!product.darwinUniversalAssetId) { + assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64'; + } else { + assetID = product.darwinUniversalAssetId; + } const url = createUpdateURL(assetID, quality); try { electron.autoUpdater.setFeedURL({ url }); diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 3ffa7bf7..d1c83416 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -56,7 +56,7 @@ export class Win32UpdateService extends AbstractUpdateService { @memoize get cachePath(): Promise { const result = path.join(tmpdir(), `vscode-update-${product.target}-${process.arch}`); - return pfs.mkdirp(result, undefined).then(() => result); + return pfs.mkdirp(result).then(() => result); } constructor( diff --git a/src/vs/platform/userDataSync/common/extensionsStorageSync.ts b/src/vs/platform/userDataSync/common/extensionsStorageSync.ts index 37846e05..3e8f5400 100644 --- a/src/vs/platform/userDataSync/common/extensionsStorageSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsStorageSync.ts @@ -5,6 +5,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; +import { adoptToGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -32,7 +33,7 @@ export class ExtensionsStorageSyncService extends Disposable implements IExtensi declare readonly _serviceBrand: undefined; private static toKey(extension: IExtensionIdWithVersion): string { - return `extensionKeys/${extension.id}@${extension.version}`; + return `extensionKeys/${adoptToGalleryExtensionId(extension.id)}@${extension.version}`; } private static fromKey(key: string): IExtensionIdWithVersion | undefined { diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index e716e135..74fe8d9c 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -11,7 +11,7 @@ import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { areSameExtensions, getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IFileService } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { merge } from 'vs/platform/userDataSync/common/extensionsMerge'; @@ -25,7 +25,7 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { CancellationToken } from 'vs/base/common/cancellation'; import { IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; import { getErrorMessage } from 'vs/base/common/errors'; -import { forEach, IStringDictionary } from 'vs/base/common/collections'; +import { IStringDictionary } from 'vs/base/common/collections'; import { IExtensionsStorageSyncService } from 'vs/platform/userDataSync/common/extensionsStorageSync'; interface IExtensionResourceMergeResult extends IAcceptResult { @@ -73,6 +73,15 @@ async function parseAndMigrateExtensions(syncData: ISyncData, extensionManagemen return extensions; } +function getExtensionStorageState(publisher: string, name: string, storageService: IStorageService): IStringDictionary { + const extensionStorageValue = storageService.get(getExtensionId(publisher, name) /* use the same id used in extension host */, StorageScope.GLOBAL) || '{}'; + return JSON.parse(extensionStorageValue); +} + +function storeExtensionStorageState(publisher: string, name: string, extensionState: IStringDictionary, storageService: IStorageService): void { + storageService.store(getExtensionId(publisher, name) /* use the same id used in extension host */, JSON.stringify(extensionState), StorageScope.GLOBAL, StorageTarget.MACHINE); +} + export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { private static readonly EXTENSIONS_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'extensions', path: `/extensions.json` }); @@ -99,7 +108,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService, - @IIgnoredExtensionsManagementService private readonly extensionSyncManagementService: IIgnoredExtensionsManagementService, + @IIgnoredExtensionsManagementService private readonly ignoredExtensionsManagementService: IIgnoredExtensionsManagementService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IConfigurationService configurationService: IConfigurationService, @@ -125,7 +134,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const installedExtensions = await this.extensionManagementService.getInstalled(); const localExtensions = this.getLocalExtensions(installedExtensions); - const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions); + const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions); if (remoteExtensions) { this.logService.trace(`${this.syncResourceLogLabel}: Merging remote extensions with local extensions...`); @@ -209,7 +218,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse private async acceptLocal(resourcePreview: IExtensionResourcePreview): Promise { const installedExtensions = await this.extensionManagementService.getInstalled(); - const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions); + const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions); const mergeResult = merge(resourcePreview.localExtensions, null, null, resourcePreview.skippedExtensions, ignoredExtensions); const { added, removed, updated, remote } = mergeResult; return { @@ -225,7 +234,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse private async acceptRemote(resourcePreview: IExtensionResourcePreview): Promise { const installedExtensions = await this.extensionManagementService.getInstalled(); - const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions); + const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions); const remoteExtensions = resourcePreview.remoteContent ? JSON.parse(resourcePreview.remoteContent) : null; if (remoteExtensions !== null) { const mergeResult = merge(resourcePreview.localExtensions, remoteExtensions, resourcePreview.localExtensions, [], ignoredExtensions); @@ -285,7 +294,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse async resolveContent(uri: URI): Promise { if (this.extUri.isEqual(uri, ExtensionsSynchroniser.EXTENSIONS_DATA_URI)) { const installedExtensions = await this.extensionManagementService.getInstalled(); - const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions); + const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions); const localExtensions = this.getLocalExtensions(installedExtensions).filter(e => !ignoredExtensions.some(id => areSameExtensions({ id }, e.identifier))); return this.format(localExtensions); } @@ -363,7 +372,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse // Builtin Extension Sync: Enablement & State if (installedExtension && installedExtension.isBuiltin) { if (e.state && installedExtension.manifest.version === e.version) { - this.updateExtensionState(e.state, e.identifier.id, installedExtension.manifest.version); + this.updateExtensionState(e.state, installedExtension.manifest.publisher, installedExtension.manifest.name, installedExtension.manifest.version); } if (e.disabled) { this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...`, e.identifier.id); @@ -382,14 +391,16 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const extension = await this.extensionGalleryService.getCompatibleExtension(e.identifier); /* Update extension state only if - * extension is installed and version is same as synced version or - * extension is not installed and installable + * extension is installed and version is same as synced version or + * extension is not installed and installable */ if (e.state && (installedExtension ? installedExtension.manifest.version === e.version /* Installed and has same version */ : !!extension /* Installable */) ) { - this.updateExtensionState(e.state, e.identifier.id, installedExtension?.manifest.version); + const publisher = installedExtension ? installedExtension.manifest.publisher : extension!.publisher; + const name = installedExtension ? installedExtension.manifest.name : extension!.name; + this.updateExtensionState(e.state, publisher, name, installedExtension?.manifest.version); } if (extension) { @@ -436,15 +447,15 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse return newSkippedExtensions; } - private updateExtensionState(state: IStringDictionary, id: string, version?: string): void { - const extensionState = JSON.parse(this.storageService.get(id, StorageScope.GLOBAL) || '{}'); - const keys = version ? this.extensionsStorageSyncService.getKeysForSync({ id, version }) : undefined; + private updateExtensionState(state: IStringDictionary, publisher: string, name: string, version: string | undefined): void { + const extensionState = getExtensionStorageState(publisher, name, this.storageService); + const keys = version ? this.extensionsStorageSyncService.getKeysForSync({ id: getGalleryExtensionId(publisher, name), version }) : undefined; if (keys) { - keys.forEach(key => extensionState[key] = state[key]); + keys.forEach(key => { extensionState[key] = state[key]; }); } else { - forEach(state, ({ key, value }) => extensionState[key] = value); + Object.keys(state).forEach(key => extensionState[key] = state[key]); } - this.storageService.store(id, JSON.stringify(extensionState), StorageScope.GLOBAL, StorageTarget.MACHINE); + storeExtensionStorageState(publisher, name, extensionState, this.storageService); } private parseExtensions(syncData: ISyncData): ISyncExtension[] { @@ -465,8 +476,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse try { const keys = this.extensionsStorageSyncService.getKeysForSync({ id: identifier.id, version: manifest.version }); if (keys) { - const extensionStorageValue = this.storageService.get(identifier.id, StorageScope.GLOBAL) || '{}'; - const extensionStorageState = JSON.parse(extensionStorageValue); + const extensionStorageState = getExtensionStorageState(manifest.publisher, manifest.name, this.storageService); syncExntesion.state = Object.keys(extensionStorageState).reduce((state: IStringDictionary, key) => { if (keys.includes(key)) { state[key] = extensionStorageState[key]; @@ -490,6 +500,7 @@ export class ExtensionsInitializer extends AbstractInitializer { @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService, @IStorageService private readonly storageService: IStorageService, + @IIgnoredExtensionsManagementService private readonly ignoredExtensionsManagementService: IIgnoredExtensionsManagementService, @IFileService fileService: IFileService, @IEnvironmentService environmentService: IEnvironmentService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @@ -511,12 +522,18 @@ export class ExtensionsInitializer extends AbstractInitializer { const newlyEnabledExtensions: ILocalExtension[] = []; const installedExtensions = await this.extensionManagementService.getInstalled(); const newExtensionsToSync = new Map(); - const installedExtensionsToSync: ISyncExtension[] = []; + const installedExtensionsToSync: { syncExtension: ISyncExtension, installedExtension: ILocalExtension }[] = []; const toInstall: { names: string[], uuids: string[] } = { names: [], uuids: [] }; const toDisable: IExtensionIdentifier[] = []; for (const extension of remoteExtensions) { - if (installedExtensions.some(i => areSameExtensions(i.identifier, extension.identifier))) { - installedExtensionsToSync.push(extension); + if (this.ignoredExtensionsManagementService.hasToNeverSyncExtension(extension.identifier.id)) { + // Skip extension ignored to sync + continue; + } + + const installedExtension = installedExtensions.find(i => areSameExtensions(i.identifier, extension.identifier)); + if (installedExtension) { + installedExtensionsToSync.push({ syncExtension: extension, installedExtension }); if (extension.disabled) { toDisable.push(extension.identifier); } @@ -528,17 +545,39 @@ export class ExtensionsInitializer extends AbstractInitializer { } else { toInstall.names.push(extension.identifier.id); } + if (extension.disabled) { + toDisable.push(extension.identifier); + } } } } + // 1. Initialise already installed extensions state + for (const { syncExtension, installedExtension } of installedExtensionsToSync) { + if (syncExtension.state) { + const extensionState = getExtensionStorageState(installedExtension.manifest.publisher, installedExtension.manifest.name, this.storageService); + Object.keys(syncExtension.state).forEach(key => extensionState[key] = syncExtension.state![key]); + storeExtensionStorageState(installedExtension.manifest.publisher, installedExtension.manifest.name, extensionState, this.storageService); + } + } + + // 2. Initialise extensions enablement + if (toDisable.length) { + for (const identifier of toDisable) { + this.logService.trace(`Disabling extension...`, identifier.id); + await this.extensionEnablementService.disableExtension(identifier); + this.logService.info(`Disabling extension`, identifier.id); + } + } + + // 3. Install extensions if (toInstall.names.length || toInstall.uuids.length) { const galleryExtensions = (await this.galleryService.query({ ids: toInstall.uuids, names: toInstall.names, pageSize: toInstall.uuids.length + toInstall.names.length }, CancellationToken.None)).firstPage; for (const galleryExtension of galleryExtensions) { try { const extensionToSync = newExtensionsToSync.get(galleryExtension.identifier.id.toLowerCase())!; if (extensionToSync.state) { - this.storageService.store(extensionToSync.identifier.id, JSON.stringify(extensionToSync.state), StorageScope.GLOBAL, StorageTarget.MACHINE); + storeExtensionStorageState(galleryExtension.publisher, galleryExtension.name, extensionToSync.state, this.storageService); } this.logService.trace(`Installing extension...`, galleryExtension.identifier.id); const local = await this.extensionManagementService.installFromGallery(galleryExtension, { isMachineScoped: false } /* pass options to prevent install and sync dialog in web */); @@ -552,22 +591,6 @@ export class ExtensionsInitializer extends AbstractInitializer { } } - if (toDisable.length) { - for (const identifier of toDisable) { - this.logService.trace(`Enabling extension...`, identifier.id); - await this.extensionEnablementService.disableExtension(identifier); - this.logService.info(`Enabled extension`, identifier.id); - } - } - - for (const extensionToSync of installedExtensionsToSync) { - if (extensionToSync.state) { - const extensionState = JSON.parse(this.storageService.get(extensionToSync.identifier.id, StorageScope.GLOBAL) || '{}'); - forEach(extensionToSync.state, ({ key, value }) => extensionState[key] = value); - this.storageService.store(extensionToSync.identifier.id, JSON.stringify(extensionState), StorageScope.GLOBAL, StorageTarget.MACHINE); - } - } - return newlyEnabledExtensions; } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 6d9d58d1..310fea18 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -31,9 +31,10 @@ export function getDisallowedIgnoredSettings(): string[] { export function getDefaultIgnoredSettings(): string[] { const allSettings = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); + const ignoreSyncSettings = Object.keys(allSettings).filter(setting => !!allSettings[setting].ignoreSync); const machineSettings = Object.keys(allSettings).filter(setting => allSettings[setting].scope === ConfigurationScope.MACHINE || allSettings[setting].scope === ConfigurationScope.MACHINE_OVERRIDABLE); const disallowedSettings = getDisallowedIgnoredSettings(); - return distinct([CONFIGURATION_SYNC_STORE_KEY, ...machineSettings, ...disallowedSettings]); + return distinct([CONFIGURATION_SYNC_STORE_KEY, ...ignoreSyncSettings, ...machineSettings, ...disallowedSettings]); } export function registerConfiguration(): IDisposable { @@ -52,10 +53,6 @@ export function registerConfiguration(): IDisposable { scope: ConfigurationScope.APPLICATION, tags: ['sync', 'usesOnlineServices'] }, - 'sync.keybindingsPerPlatform': { - type: 'boolean', - deprecationMessage: localize('sync.keybindingsPerPlatform.deprecated', "Deprecated, use settingsSync.keybindingsPerPlatform instead"), - }, 'settingsSync.ignoredExtensions': { 'type': 'array', markdownDescription: localize('settingsSync.ignoredExtensions', "List of extensions to be ignored while synchronizing. The identifier of an extension is always `${publisher}.${name}`. For example: `vscode.csharp`."), @@ -70,10 +67,6 @@ export function registerConfiguration(): IDisposable { disallowSyncIgnore: true, tags: ['sync', 'usesOnlineServices'] }, - 'sync.ignoredExtensions': { - 'type': 'array', - deprecationMessage: localize('sync.ignoredExtensions.deprecated', "Deprecated, use settingsSync.ignoredExtensions instead"), - }, 'settingsSync.ignoredSettings': { 'type': 'array', description: localize('settingsSync.ignoredSettings', "Configure settings to be ignored while synchronizing."), @@ -84,10 +77,6 @@ export function registerConfiguration(): IDisposable { uniqueItems: true, disallowSyncIgnore: true, tags: ['sync', 'usesOnlineServices'] - }, - 'sync.ignoredSettings': { - 'type': 'array', - deprecationMessage: localize('sync.ignoredSettings.deprecated', "Deprecated, use settingsSync.ignoredSettings instead"), } } }); diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 2cf4ac37..ad5494bc 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -445,131 +445,171 @@ class ManualSyncTask extends Disposable implements IManualSyncTask { } async preview(): Promise<[SyncResource, ISyncResourcePreview][]> { - if (this.isDisposed) { - throw new Error('Disposed'); + try { + if (this.isDisposed) { + throw new Error('Disposed'); + } + if (!this.previewsPromise) { + this.previewsPromise = createCancelablePromise(token => this.getPreviews(token)); + } + if (!this.previews) { + this.previews = await this.previewsPromise; + } + return this.previews; + } catch (error) { + this.logService.error(error); + throw error; } - if (!this.previewsPromise) { - this.previewsPromise = createCancelablePromise(token => this.getPreviews(token)); - } - if (!this.previews) { - this.previews = await this.previewsPromise; - } - return this.previews; } async accept(resource: URI, content?: string | null): Promise<[SyncResource, ISyncResourcePreview][]> { - return this.performAction(resource, sychronizer => sychronizer.accept(resource, content)); + try { + return await this.performAction(resource, sychronizer => sychronizer.accept(resource, content)); + } catch (error) { + this.logService.error(error); + throw error; + } } async merge(resource?: URI): Promise<[SyncResource, ISyncResourcePreview][]> { - if (resource) { - return this.performAction(resource, sychronizer => sychronizer.merge(resource)); - } else { - return this.mergeAll(); + try { + if (resource) { + return await this.performAction(resource, sychronizer => sychronizer.merge(resource)); + } else { + return await this.mergeAll(); + } + } catch (error) { + this.logService.error(error); + throw error; } } async discard(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]> { - return this.performAction(resource, sychronizer => sychronizer.discard(resource)); + try { + return await this.performAction(resource, sychronizer => sychronizer.discard(resource)); + } catch (error) { + this.logService.error(error); + throw error; + } } async discardConflicts(): Promise<[SyncResource, ISyncResourcePreview][]> { - if (!this.previews) { - throw new Error('Missing preview. Create preview and try again.'); - } - if (this.synchronizingResources.length) { - throw new Error('Cannot discard while synchronizing resources'); - } + try { + if (!this.previews) { + throw new Error('Missing preview. Create preview and try again.'); + } + if (this.synchronizingResources.length) { + throw new Error('Cannot discard while synchronizing resources'); + } - const conflictResources: URI[] = []; - for (const [, syncResourcePreview] of this.previews) { - for (const resourcePreview of syncResourcePreview.resourcePreviews) { - if (resourcePreview.mergeState === MergeState.Conflict) { - conflictResources.push(resourcePreview.previewResource); + const conflictResources: URI[] = []; + for (const [, syncResourcePreview] of this.previews) { + for (const resourcePreview of syncResourcePreview.resourcePreviews) { + if (resourcePreview.mergeState === MergeState.Conflict) { + conflictResources.push(resourcePreview.previewResource); + } } } - } - for (const resource of conflictResources) { - await this.discard(resource); + for (const resource of conflictResources) { + await this.discard(resource); + } + return this.previews; + } catch (error) { + this.logService.error(error); + throw error; } - return this.previews; } async apply(): Promise<[SyncResource, ISyncResourcePreview][]> { - if (!this.previews) { - throw new Error('You need to create preview before applying'); - } - if (this.synchronizingResources.length) { - throw new Error('Cannot pull while synchronizing resources'); - } - const previews: [SyncResource, ISyncResourcePreview][] = []; - for (const [syncResource, preview] of this.previews) { - this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); - this._onSynchronizeResources.fire(this.synchronizingResources); + try { + if (!this.previews) { + throw new Error('You need to create preview before applying'); + } + if (this.synchronizingResources.length) { + throw new Error('Cannot pull while synchronizing resources'); + } + const previews: [SyncResource, ISyncResourcePreview][] = []; + for (const [syncResource, preview] of this.previews) { + this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); + this._onSynchronizeResources.fire(this.synchronizingResources); - const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; + const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; - /* merge those which are not yet merged */ - for (const resourcePreview of preview.resourcePreviews) { - if ((resourcePreview.localChange !== Change.None || resourcePreview.remoteChange !== Change.None) && resourcePreview.mergeState === MergeState.Preview) { - await synchroniser.merge(resourcePreview.previewResource); + /* merge those which are not yet merged */ + for (const resourcePreview of preview.resourcePreviews) { + if ((resourcePreview.localChange !== Change.None || resourcePreview.remoteChange !== Change.None) && resourcePreview.mergeState === MergeState.Preview) { + await synchroniser.merge(resourcePreview.previewResource); + } } - } - /* apply */ - const newPreview = await synchroniser.apply(false, this.syncHeaders); - if (newPreview) { - previews.push(this.toSyncResourcePreview(synchroniser.resource, newPreview)); - } + /* apply */ + const newPreview = await synchroniser.apply(false, this.syncHeaders); + if (newPreview) { + previews.push(this.toSyncResourcePreview(synchroniser.resource, newPreview)); + } - this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); - this._onSynchronizeResources.fire(this.synchronizingResources); + this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); + this._onSynchronizeResources.fire(this.synchronizingResources); + } + this.previews = previews; + return this.previews; + } catch (error) { + this.logService.error(error); + throw error; } - this.previews = previews; - return this.previews; } async pull(): Promise { - if (!this.previews) { - throw new Error('You need to create preview before applying'); - } - if (this.synchronizingResources.length) { - throw new Error('Cannot pull while synchronizing resources'); - } - for (const [syncResource, preview] of this.previews) { - this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); - this._onSynchronizeResources.fire(this.synchronizingResources); - const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; - for (const resourcePreview of preview.resourcePreviews) { - await synchroniser.accept(resourcePreview.remoteResource); + try { + if (!this.previews) { + throw new Error('You need to create preview before applying'); } - await synchroniser.apply(true, this.syncHeaders); - this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); - this._onSynchronizeResources.fire(this.synchronizingResources); + if (this.synchronizingResources.length) { + throw new Error('Cannot pull while synchronizing resources'); + } + for (const [syncResource, preview] of this.previews) { + this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); + this._onSynchronizeResources.fire(this.synchronizingResources); + const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; + for (const resourcePreview of preview.resourcePreviews) { + await synchroniser.accept(resourcePreview.remoteResource); + } + await synchroniser.apply(true, this.syncHeaders); + this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); + this._onSynchronizeResources.fire(this.synchronizingResources); + } + this.previews = []; + } catch (error) { + this.logService.error(error); + throw error; } - this.previews = []; } async push(): Promise { - if (!this.previews) { - throw new Error('You need to create preview before applying'); - } - if (this.synchronizingResources.length) { - throw new Error('Cannot pull while synchronizing resources'); - } - for (const [syncResource, preview] of this.previews) { - this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); - this._onSynchronizeResources.fire(this.synchronizingResources); - const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; - for (const resourcePreview of preview.resourcePreviews) { - await synchroniser.accept(resourcePreview.localResource); + try { + if (!this.previews) { + throw new Error('You need to create preview before applying'); } - await synchroniser.apply(true, this.syncHeaders); - this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); - this._onSynchronizeResources.fire(this.synchronizingResources); + if (this.synchronizingResources.length) { + throw new Error('Cannot pull while synchronizing resources'); + } + for (const [syncResource, preview] of this.previews) { + this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); + this._onSynchronizeResources.fire(this.synchronizingResources); + const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; + for (const resourcePreview of preview.resourcePreviews) { + await synchroniser.accept(resourcePreview.localResource); + } + await synchroniser.apply(true, this.syncHeaders); + this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); + this._onSynchronizeResources.fire(this.synchronizingResources); + } + this.previews = []; + } catch (error) { + this.logService.error(error); + throw error; } - this.previews = []; } async stop(): Promise { diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index 40958689..a4bde40f 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable, } from 'vs/base/common/lifecycle'; import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle, HEADER_OPERATION_ID, HEADER_EXECUTION_ID, CONFIGURATION_SYNC_STORE_KEY, IAuthenticationProvider, IUserDataSyncStoreManagementService, UserDataSyncStoreType, IUserDataSyncStoreClient } from 'vs/platform/userDataSync/common/userDataSync'; import { IRequestService, asText, isSuccess as isSuccessContext, asJson } from 'vs/platform/request/common/request'; import { joinPath, relativePath } from 'vs/base/common/resources'; @@ -180,6 +180,12 @@ export class UserDataSyncStoreClient extends Disposable implements IUserDataSync /* A requests session that limits requests per sessions */ this.session = new RequestsSession(REQUEST_SESSION_LIMIT, REQUEST_SESSION_INTERVAL, this.requestService, this.logService); this.initDonotMakeRequestsUntil(); + this._register(toDisposable(() => { + if (this.resetDonotMakeRequestsUntilPromise) { + this.resetDonotMakeRequestsUntilPromise.cancel(); + this.resetDonotMakeRequestsUntilPromise = undefined; + } + })); } setAuthToken(token: string, type: string): void { @@ -210,6 +216,7 @@ export class UserDataSyncStoreClient extends Disposable implements IUserDataSync if (this._donotMakeRequestsUntil) { this.storageService.store(DONOT_MAKE_REQUESTS_UNTIL_KEY, this._donotMakeRequestsUntil.getTime(), StorageScope.GLOBAL, StorageTarget.MACHINE); this.resetDonotMakeRequestsUntilPromise = createCancelablePromise(token => timeout(this._donotMakeRequestsUntil!.getTime() - Date.now(), token).then(() => this.setDonotMakeRequestsUntil(undefined))); + this.resetDonotMakeRequestsUntilPromise.then(null, e => null /* ignore error */); } else { this.storageService.remove(DONOT_MAKE_REQUESTS_UNTIL_KEY, StorageScope.GLOBAL); } diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index 5d2731d5..24739111 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -181,7 +181,7 @@ suite('TestSynchronizer - Auto Sync', () => { teardown(() => disposableStore.clear()); test('status is syncing', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); @@ -198,7 +198,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('status is set correctly when sync is finished', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); const actual: SyncStatus[] = []; @@ -210,7 +210,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('status is set correctly when sync has errors', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasError: true, hasConflicts: false }; testObject.syncBarrier.open(); @@ -227,7 +227,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('status is set to hasConflicts when asked to sync if there are conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -238,7 +238,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('sync should not run if syncing already', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); const promise = Event.toPromise(testObject.onDoSyncCall.event); testObject.sync(await client.manifest()); @@ -255,7 +255,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('sync should not run if disabled', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); client.instantiationService.get(IUserDataSyncResourceEnablementService).setResourceEnablement(testObject.resource, false); const actual: SyncStatus[] = []; @@ -268,7 +268,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('sync should not run if there are conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -282,7 +282,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('accept preview during conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -300,7 +300,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('accept remote during conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); const fileService = client.instantiationService.get(IFileService); @@ -323,7 +323,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('accept local during conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); const fileService = client.instantiationService.get(IFileService); @@ -345,7 +345,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('accept new content during conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); const fileService = client.instantiationService.get(IFileService); @@ -368,7 +368,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('accept delete during conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); const fileService = client.instantiationService.get(IFileService); @@ -390,7 +390,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('accept deleted local during conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); const fileService = client.instantiationService.get(IFileService); @@ -411,7 +411,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('accept deleted remote during conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); const fileService = client.instantiationService.get(IFileService); await fileService.writeFile(testObject.localResource, VSBuffer.fromString('some content')); @@ -431,7 +431,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('request latest data on precondition failure', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); // Sync once testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -458,7 +458,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('no requests are made to server when local change is triggered', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -471,7 +471,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('status is reset when getting latest remote data fails', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.failWhenGettingLatestRemoteUserData = true; try { @@ -502,7 +502,7 @@ suite('TestSynchronizer - Manual Sync', () => { teardown(() => disposableStore.clear()); test('preview', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -514,7 +514,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> merge', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -528,7 +528,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -542,7 +542,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> merge -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -557,7 +557,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> merge -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -577,7 +577,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -597,7 +597,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> merge -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -617,7 +617,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -630,7 +630,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -650,7 +650,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> merge -> discard', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -665,7 +665,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> merge -> discard -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -681,7 +681,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> accept -> discard', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -696,7 +696,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> accept -> discard -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -712,7 +712,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> accept -> discard -> merge', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -728,7 +728,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> merge -> accept -> discard', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -744,7 +744,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> merge -> discard -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -764,7 +764,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> accept -> discard -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -785,7 +785,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> accept -> discard -> merge -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -808,7 +808,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preview', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -820,7 +820,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preview -> merge', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -834,7 +834,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preview -> merge -> discard', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -849,7 +849,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preview -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -864,7 +864,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preview -> merge -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -887,7 +887,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preview -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -901,7 +901,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preview -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -923,7 +923,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> merge -> discard', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -938,7 +938,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> merge -> discard -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -954,7 +954,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> accept -> discard', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -969,7 +969,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> accept -> discard -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -985,7 +985,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> accept -> discard -> merge', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -1001,7 +1001,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> merge -> discard -> merge', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -1017,7 +1017,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> merge -> accept -> discard', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -1033,7 +1033,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> merge -> discard -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -1053,7 +1053,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> accept -> discard -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); diff --git a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts index 49df9ea7..a12085fe 100644 --- a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts @@ -40,7 +40,7 @@ suite('UserDataAutoSyncService', () => { await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run(); target.reset(); - const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Trigger auto sync with settings change await testObject.triggerSync([SyncResource.Settings], false, false); @@ -62,7 +62,7 @@ suite('UserDataAutoSyncService', () => { await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run(); target.reset(); - const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Trigger auto sync with settings change multiple times for (let counter = 0; counter < 2; counter++) { @@ -88,7 +88,7 @@ suite('UserDataAutoSyncService', () => { await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run(); target.reset(); - const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Trigger auto sync with window focus once await testObject.triggerSync(['windowFocus'], true, false); @@ -110,7 +110,7 @@ suite('UserDataAutoSyncService', () => { await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run(); target.reset(); - const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Trigger auto sync with window focus multiple times for (let counter = 0; counter < 2; counter++) { @@ -129,7 +129,7 @@ suite('UserDataAutoSyncService', () => { const target = new UserDataSyncTestServer(); const client = disposableStore.add(new UserDataSyncClient(target)); await client.setUp(); - const testObject: TestUserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); await testObject.sync(); @@ -165,7 +165,7 @@ suite('UserDataAutoSyncService', () => { const target = new UserDataSyncTestServer(); const client = disposableStore.add(new UserDataSyncClient(target)); await client.setUp(); - const testObject: TestUserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Sync once and reset requests await testObject.sync(); @@ -185,7 +185,7 @@ suite('UserDataAutoSyncService', () => { const target = new UserDataSyncTestServer(); const client = disposableStore.add(new UserDataSyncClient(target)); await client.setUp(); - const testObject: TestUserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Sync once and reset requests await testObject.sync(); @@ -220,7 +220,7 @@ suite('UserDataAutoSyncService', () => { const target = new UserDataSyncTestServer(); const client = disposableStore.add(new UserDataSyncClient(target)); await client.setUp(); - const testObject: TestUserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Sync once and reset requests await testObject.sync(); @@ -250,7 +250,7 @@ suite('UserDataAutoSyncService', () => { // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); await testObject.sync(); // Reset from the first client @@ -279,7 +279,7 @@ suite('UserDataAutoSyncService', () => { // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); await testObject.sync(); // Disable current machine @@ -311,7 +311,7 @@ suite('UserDataAutoSyncService', () => { // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); await testObject.sync(); // Remove current machine @@ -339,7 +339,7 @@ suite('UserDataAutoSyncService', () => { // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); await testObject.sync(); // Reset from the first client @@ -371,7 +371,7 @@ suite('UserDataAutoSyncService', () => { // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); const errorPromise = Event.toPromise(testObject.onError); while (target.requests.length < 5) { @@ -389,7 +389,7 @@ suite('UserDataAutoSyncService', () => { // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); while (target.requests.length < 5) { await testObject.sync(); @@ -407,7 +407,7 @@ suite('UserDataAutoSyncService', () => { // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); await testObject.triggerSync(['some reason'], true, true); assert.equal(target.requestsWithAllHeaders[0].headers!['Cache-Control'], 'no-cache'); @@ -419,7 +419,7 @@ suite('UserDataAutoSyncService', () => { // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); await testObject.triggerSync(['some reason'], true, false); assert.equal(target.requestsWithAllHeaders[0].headers!['Cache-Control'], undefined); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index 32204697..b9d4381d 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -83,9 +83,9 @@ export class UserDataSyncClient extends Disposable { fileService.registerProvider(Schemas.inMemory, new InMemoryFileSystemProvider()); this.instantiationService.stub(IFileService, fileService); - this.instantiationService.stub(IStorageService, new InMemoryStorageService()); + this.instantiationService.stub(IStorageService, this._register(new InMemoryStorageService())); - const configurationService = new ConfigurationService(environmentService.settingsResource, fileService); + const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService)); await configurationService.initialize(); this.instantiationService.stub(IConfigurationService, configurationService); @@ -93,20 +93,20 @@ export class UserDataSyncClient extends Disposable { this.instantiationService.stub(IUserDataSyncLogService, logService); this.instantiationService.stub(ITelemetryService, NullTelemetryService); - this.instantiationService.stub(IUserDataSyncStoreManagementService, this.instantiationService.createInstance(UserDataSyncStoreManagementService)); - this.instantiationService.stub(IUserDataSyncStoreService, this.instantiationService.createInstance(UserDataSyncStoreService)); + this.instantiationService.stub(IUserDataSyncStoreManagementService, this._register(this.instantiationService.createInstance(UserDataSyncStoreManagementService))); + this.instantiationService.stub(IUserDataSyncStoreService, this._register(this.instantiationService.createInstance(UserDataSyncStoreService))); - const userDataSyncAccountService: IUserDataSyncAccountService = this.instantiationService.createInstance(UserDataSyncAccountService); + const userDataSyncAccountService: IUserDataSyncAccountService = this._register(this.instantiationService.createInstance(UserDataSyncAccountService)); await userDataSyncAccountService.updateAccount({ authenticationProviderId: 'authenticationProviderId', token: 'token' }); this.instantiationService.stub(IUserDataSyncAccountService, userDataSyncAccountService); - this.instantiationService.stub(IUserDataSyncMachinesService, this.instantiationService.createInstance(UserDataSyncMachinesService)); - this.instantiationService.stub(IUserDataSyncBackupStoreService, this.instantiationService.createInstance(UserDataSyncBackupStoreService)); + this.instantiationService.stub(IUserDataSyncMachinesService, this._register(this.instantiationService.createInstance(UserDataSyncMachinesService))); + this.instantiationService.stub(IUserDataSyncBackupStoreService, this._register(this.instantiationService.createInstance(UserDataSyncBackupStoreService))); this.instantiationService.stub(IUserDataSyncUtilService, new TestUserDataSyncUtilService()); - this.instantiationService.stub(IUserDataSyncResourceEnablementService, this.instantiationService.createInstance(UserDataSyncResourceEnablementService)); + this.instantiationService.stub(IUserDataSyncResourceEnablementService, this._register(this.instantiationService.createInstance(UserDataSyncResourceEnablementService))); - this.instantiationService.stub(IGlobalExtensionEnablementService, this.instantiationService.createInstance(GlobalExtensionEnablementService)); - this.instantiationService.stub(IExtensionsStorageSyncService, this.instantiationService.createInstance(ExtensionsStorageSyncService)); + this.instantiationService.stub(IGlobalExtensionEnablementService, this._register(this.instantiationService.createInstance(GlobalExtensionEnablementService))); + this.instantiationService.stub(IExtensionsStorageSyncService, this._register(this.instantiationService.createInstance(ExtensionsStorageSyncService))); this.instantiationService.stub(IIgnoredExtensionsManagementService, this.instantiationService.createInstance(IgnoredExtensionsManagementService)); this.instantiationService.stub(IExtensionManagementService, >{ async getInstalled() { return []; }, @@ -118,8 +118,8 @@ export class UserDataSyncClient extends Disposable { async getCompatibleExtension() { return null; } }); - this.instantiationService.stub(IUserDataAutoSyncEnablementService, this.instantiationService.createInstance(UserDataAutoSyncEnablementService)); - this.instantiationService.stub(IUserDataSyncService, this.instantiationService.createInstance(UserDataSyncService)); + this.instantiationService.stub(IUserDataAutoSyncEnablementService, this._register(this.instantiationService.createInstance(UserDataAutoSyncEnablementService))); + this.instantiationService.stub(IUserDataSyncService, this._register(this.instantiationService.createInstance(UserDataSyncService))); if (!empty) { await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({}))); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts index 2c7d1734..4550c939 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts @@ -58,7 +58,7 @@ suite('UserDataSyncStoreManagementService', () => { authenticationProviders: [{ id: 'configuredAuthProvider', scopes: [] }] }; - const testObject: IUserDataSyncStoreManagementService = client.instantiationService.createInstance(UserDataSyncStoreManagementService); + const testObject: IUserDataSyncStoreManagementService = disposableStore.add(client.instantiationService.createInstance(UserDataSyncStoreManagementService)); assert.equal(testObject.userDataSyncStore?.url.toString(), expected.url.toString()); assert.equal(testObject.userDataSyncStore?.defaultUrl.toString(), expected.defaultUrl.toString()); @@ -419,7 +419,7 @@ suite('UserDataSyncStoreService', () => { await testObject.manifest(); } catch (e) { } - const target = client.instantiationService.createInstance(UserDataSyncStoreService); + const target = disposableStore.add(client.instantiationService.createInstance(UserDataSyncStoreService)); assert.equal(target.donotMakeRequestsUntil?.getTime(), testObject.donotMakeRequestsUntil?.getTime()); }); @@ -434,7 +434,7 @@ suite('UserDataSyncStoreService', () => { } catch (e) { } await timeout(300); - const target = client.instantiationService.createInstance(UserDataSyncStoreService); + const target = disposableStore.add(client.instantiationService.createInstance(UserDataSyncStoreService)); assert.ok(!target.donotMakeRequestsUntil); }); diff --git a/src/vs/platform/webview/common/resourceLoader.ts b/src/vs/platform/webview/common/resourceLoader.ts index ee64c8ef..46f0217f 100644 --- a/src/vs/platform/webview/common/resourceLoader.ts +++ b/src/vs/platform/webview/common/resourceLoader.ts @@ -9,6 +9,7 @@ import { isUNC } from 'vs/base/common/extpath'; import { Schemas } from 'vs/base/common/network'; import { sep } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; +import { ILogService } from 'vs/platform/log/common/log'; import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IRequestService } from 'vs/platform/request/common/request'; import { getWebviewContentMimeType } from 'vs/platform/webview/common/mimeTypes'; @@ -24,7 +25,8 @@ export namespace WebviewResourceResponse { constructor( public readonly stream: VSBufferReadableStream, - public readonly mimeType: string + public readonly etag: string | undefined, + public readonly mimeType: string, ) { } } @@ -35,7 +37,7 @@ export namespace WebviewResourceResponse { } interface FileReader { - readFileStream(resource: URI): Promise; + readFileStream(resource: URI): Promise<{ stream: VSBufferReadableStream, etag?: string }>; } export async function loadLocalResource( @@ -48,8 +50,14 @@ export async function loadLocalResource( }, fileReader: FileReader, requestService: IRequestService, + logService: ILogService, ): Promise { + logService.debug(`loadLocalResource - being. requestUri=${requestUri}`); + let resourceToLoad = getResourceToLoad(requestUri, options.roots); + + logService.debug(`loadLocalResource - found resource to load. requestUri=${requestUri}, resourceToLoad=${resourceToLoad}`); + if (!resourceToLoad) { return WebviewResourceResponse.AccessDenied; } @@ -63,17 +71,23 @@ export async function loadLocalResource( if (resourceToLoad.scheme === Schemas.http || resourceToLoad.scheme === Schemas.https) { const response = await requestService.request({ url: resourceToLoad.toString(true) }, CancellationToken.None); + logService.debug(`loadLocalResource - Loaded over http(s). requestUri=${requestUri}, response=${response.res.statusCode}`); + if (response.res.statusCode === 200) { - return new WebviewResourceResponse.StreamSuccess(response.stream, mime); + return new WebviewResourceResponse.StreamSuccess(response.stream, undefined, mime); } return WebviewResourceResponse.Failed; } try { const contents = await fileReader.readFileStream(resourceToLoad); - return new WebviewResourceResponse.StreamSuccess(contents, mime); + logService.debug(`loadLocalResource - Loaded using fileReader. requestUri=${requestUri}`); + + return new WebviewResourceResponse.StreamSuccess(contents.stream, contents.etag, mime); } catch (err) { + logService.debug(`loadLocalResource - Error using fileReader. requestUri=${requestUri}`); console.log(err); + return WebviewResourceResponse.Failed; } } diff --git a/src/vs/platform/webview/common/webviewPortMapping.ts b/src/vs/platform/webview/common/webviewPortMapping.ts index 58154313..4e9c2297 100644 --- a/src/vs/platform/webview/common/webviewPortMapping.ts +++ b/src/vs/platform/webview/common/webviewPortMapping.ts @@ -19,7 +19,7 @@ export interface IWebviewPortMapping { */ export class WebviewPortMappingManager implements IDisposable { - private readonly _tunnels = new Map>(); + private readonly _tunnels = new Map(); constructor( private readonly _getExtensionLocation: () => URI | undefined, @@ -60,19 +60,19 @@ export class WebviewPortMappingManager implements IDisposable { return undefined; } - dispose() { + async dispose() { for (const tunnel of this._tunnels.values()) { - tunnel.then(tunnel => tunnel.dispose()); + await tunnel.dispose(); } this._tunnels.clear(); } - private getOrCreateTunnel(remoteAuthority: IAddress, remotePort: number): Promise | undefined { + private async getOrCreateTunnel(remoteAuthority: IAddress, remotePort: number): Promise { const existing = this._tunnels.get(remotePort); if (existing) { return existing; } - const tunnel = this.tunnelService.openTunnel({ getAddress: async () => remoteAuthority }, undefined, remotePort); + const tunnel = await this.tunnelService.openTunnel({ getAddress: async () => remoteAuthority }, undefined, remotePort); if (tunnel) { this._tunnels.set(remotePort, tunnel); } diff --git a/src/vs/platform/webview/electron-main/webviewMainService.ts b/src/vs/platform/webview/electron-main/webviewMainService.ts index 03a6e306..92bd47e2 100644 --- a/src/vs/platform/webview/electron-main/webviewMainService.ts +++ b/src/vs/platform/webview/electron-main/webviewMainService.ts @@ -8,6 +8,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { IRequestService } from 'vs/platform/request/common/request'; import { IWebviewManagerService, RegisterWebviewMetadata, WebviewWebContentsId, WebviewWindowId } from 'vs/platform/webview/common/webviewManagerService'; @@ -24,12 +25,13 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer constructor( @IFileService fileService: IFileService, + @ILogService logService: ILogService, @IRequestService requestService: IRequestService, @ITunnelService tunnelService: ITunnelService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, ) { super(); - this.protocolProvider = this._register(new WebviewProtocolProvider(fileService, requestService, windowsMainService)); + this.protocolProvider = this._register(new WebviewProtocolProvider(fileService, logService, requestService, windowsMainService)); this.portMappingProvider = this._register(new WebviewPortMappingProvider(tunnelService)); } diff --git a/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts b/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts index a019d267..a8c2e0fe 100644 --- a/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts +++ b/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts @@ -10,6 +10,7 @@ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { FileAccess, Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IRequestService } from 'vs/platform/request/common/request'; import { loadLocalResource, webviewPartitionId, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader'; @@ -38,8 +39,9 @@ export class WebviewProtocolProvider extends Disposable { constructor( @IFileService private readonly fileService: IFileService, + @ILogService private readonly logService: ILogService, @IRequestService private readonly requestService: IRequestService, - @IWindowsMainService readonly windowsMainService: IWindowsMainService, + @IWindowsMainService private readonly windowsMainService: IWindowsMainService, ) { super(); @@ -125,7 +127,10 @@ export class WebviewProtocolProvider extends Disposable { } } - private async handleWebviewRequest(request: Electron.Request, callback: any) { + private async handleWebviewRequest( + request: Electron.ProtocolRequest, + callback: (response: string | Electron.ProtocolResponse) => void + ) { try { const uri = URI.parse(request.url); const entry = WebviewProtocolProvider.validWebviewFilePaths.get(uri.path); @@ -144,8 +149,8 @@ export class WebviewProtocolProvider extends Disposable { } private async handleWebviewResourceRequest( - request: Electron.Request, - callback: (stream?: NodeJS.ReadableStream | Electron.StreamProtocolResponse | undefined) => void + request: Electron.ProtocolRequest, + callback: (stream: NodeJS.ReadableStream | Electron.ProtocolResponse) => void ) { try { const uri = URI.parse(request.url); @@ -170,10 +175,14 @@ export class WebviewProtocolProvider extends Disposable { }; } - const fileService = { - readFileStream: async (resource: URI): Promise => { + const fileReader = { + readFileStream: async (resource: URI): Promise<{ stream: VSBufferReadableStream, etag?: string }> => { if (resource.scheme === Schemas.file) { - return (await this.fileService.readFileStream(resource)).value; + const result = (await this.fileService.readFileStream(resource)); + return { + stream: result.value, + etag: result.etag + }; } // Unknown uri scheme. Try delegating the file read back to the renderer @@ -196,7 +205,7 @@ export class WebviewProtocolProvider extends Disposable { throw new FileOperationError('Could not read file', FileOperationResult.FILE_NOT_FOUND); } - return bufferToStream(result); + return { stream: bufferToStream(result), etag: undefined }; } }; @@ -205,29 +214,55 @@ export class WebviewProtocolProvider extends Disposable { roots: metadata.localResourceRoots, remoteConnectionData: metadata.remoteConnectionData, rewriteUri, - }, fileService, this.requestService); + }, fileReader, this.requestService, this.logService); if (result.type === WebviewResourceResponse.Type.Success) { + const cacheHeaders: Record = result.etag ? { + 'ETag': result.etag, + 'Cache-Control': 'no-cache' + } : {}; + + const ifNoneMatch = request.headers['If-None-Match']; + if (ifNoneMatch && result.etag === ifNoneMatch) { + /* + * Note that the server generating a 304 response MUST + * generate any of the following header fields that would + * have been sent in a 200 (OK) response to the same request: + * Cache-Control, Content-Location, Date, ETag, Expires, and Vary. + * (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) + */ + return callback({ + statusCode: 304, // not modified + data: undefined, // The request fails if `data` is not set + headers: { + 'Content-Type': result.mimeType, + 'Access-Control-Allow-Origin': '*', + ...cacheHeaders + } + }); + } + return callback({ statusCode: 200, data: this.streamToNodeReadable(result.stream), headers: { 'Content-Type': result.mimeType, 'Access-Control-Allow-Origin': '*', + ...cacheHeaders } }); } if (result.type === WebviewResourceResponse.Type.AccessDenied) { console.error('Webview: Cannot load resource outside of protocol root'); - return callback({ data: null, statusCode: 401 }); + return callback({ data: undefined, statusCode: 401 }); } } } catch { // noop } - return callback({ data: null, statusCode: 404 }); + return callback({ data: undefined, statusCode: 404 }); } public didLoadResource(requestId: number, content: VSBuffer | undefined) { diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 40c95213..f97741d1 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -9,7 +9,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { LogLevel } from 'vs/platform/log/common/log'; -import { ExportData } from 'vs/base/common/performance'; +import { PerformanceMark } from 'vs/base/common/performance'; export const WindowMinimumSize = { WIDTH: 400, @@ -18,38 +18,37 @@ export const WindowMinimumSize = { }; export interface IBaseOpenWindowsOptions { - forceReuseWindow?: boolean; + readonly forceReuseWindow?: boolean; } export interface IOpenWindowOptions extends IBaseOpenWindowsOptions { - forceNewWindow?: boolean; - preferNewWindow?: boolean; + readonly forceNewWindow?: boolean; + readonly preferNewWindow?: boolean; - noRecentEntry?: boolean; + readonly noRecentEntry?: boolean; - addMode?: boolean; + readonly addMode?: boolean; - diffMode?: boolean; - gotoLineMode?: boolean; + readonly diffMode?: boolean; + readonly gotoLineMode?: boolean; - waitMarkerFileURI?: URI; + readonly waitMarkerFileURI?: URI; } export interface IAddFoldersRequest { - foldersToAdd: UriComponents[]; + readonly foldersToAdd: UriComponents[]; } export interface IOpenedWindow { - id: number; - workspace?: IWorkspaceIdentifier; - folderUri?: ISingleFolderWorkspaceIdentifier; - title: string; - filename?: string; - dirty: boolean; + readonly id: number; + readonly workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; + readonly title: string; + readonly filename?: string; + readonly dirty: boolean; } export interface IOpenEmptyWindowOptions extends IBaseOpenWindowsOptions { - remoteAuthority?: string; + readonly remoteAuthority?: string; } export type IWindowOpenable = IWorkspaceToOpen | IFolderToOpen | IFileToOpen; @@ -59,15 +58,15 @@ export interface IBaseWindowOpenable { } export interface IWorkspaceToOpen extends IBaseWindowOpenable { - workspaceUri: URI; + readonly workspaceUri: URI; } export interface IFolderToOpen extends IBaseWindowOpenable { - folderUri: URI; + readonly folderUri: URI; } export interface IFileToOpen extends IBaseWindowOpenable { - fileUri: URI; + readonly fileUri: URI; } export function isWorkspaceToOpen(uriToOpen: IWindowOpenable): uriToOpen is IWorkspaceToOpen { @@ -96,26 +95,25 @@ export function getMenuBarVisibility(configurationService: IConfigurationService } export interface IWindowsConfiguration { - window: IWindowSettings; + readonly window: IWindowSettings; } export interface IWindowSettings { - openFilesInNewWindow: 'on' | 'off' | 'default'; - openFoldersInNewWindow: 'on' | 'off' | 'default'; - openWithoutArgumentsInNewWindow: 'on' | 'off'; - restoreWindows: 'preserve' | 'all' | 'folders' | 'one' | 'none'; - restoreFullscreen: boolean; - zoomLevel: number; - titleBarStyle: 'native' | 'custom'; - autoDetectHighContrast: boolean; - menuBarVisibility: MenuBarVisibility; - newWindowDimensions: 'default' | 'inherit' | 'offset' | 'maximized' | 'fullscreen'; - nativeTabs: boolean; - nativeFullScreen: boolean; - enableMenuBarMnemonics: boolean; - closeWhenEmpty: boolean; - clickThroughInactive: boolean; - enableExperimentalProxyLoginDialog: boolean; + readonly openFilesInNewWindow: 'on' | 'off' | 'default'; + readonly openFoldersInNewWindow: 'on' | 'off' | 'default'; + readonly openWithoutArgumentsInNewWindow: 'on' | 'off'; + readonly restoreWindows: 'preserve' | 'all' | 'folders' | 'one' | 'none'; + readonly restoreFullscreen: boolean; + readonly zoomLevel: number; + readonly titleBarStyle: 'native' | 'custom'; + readonly autoDetectHighContrast: boolean; + readonly menuBarVisibility: MenuBarVisibility; + readonly newWindowDimensions: 'default' | 'inherit' | 'offset' | 'maximized' | 'fullscreen'; + readonly nativeTabs: boolean; + readonly nativeFullScreen: boolean; + readonly enableMenuBarMnemonics: boolean; + readonly closeWhenEmpty: boolean; + readonly clickThroughInactive: boolean; } export function getTitleBarStyle(configurationService: IConfigurationService): 'native' | 'custom' { @@ -154,24 +152,24 @@ export interface IPath extends IPathData { export interface IPathData { // the file path to open within the instance - fileUri?: UriComponents; + readonly fileUri?: UriComponents; // the line number in the file path to open - lineNumber?: number; + readonly lineNumber?: number; // the column number in the file path to open - columnNumber?: number; + readonly columnNumber?: number; // a hint that the file exists. if true, the // file exists, if false it does not. with // undefined the state is unknown. - exists?: boolean; + readonly exists?: boolean; // Specifies if the file should be only be opened if it exists - openOnlyIfExists?: boolean; + readonly openOnlyIfExists?: boolean; // Specifies an optional id to override the editor used to edit the resource, e.g. custom editor. - overrideId?: string; + readonly overrideId?: string; } export interface IPathsToWaitFor extends IPathsToWaitForData { @@ -180,36 +178,36 @@ export interface IPathsToWaitFor extends IPathsToWaitForData { } interface IPathsToWaitForData { - paths: IPathData[]; - waitMarkerFileUri: UriComponents; + readonly paths: IPathData[]; + readonly waitMarkerFileUri: UriComponents; } export interface IOpenFileRequest { - filesToOpenOrCreate?: IPathData[]; - filesToDiff?: IPathData[]; + readonly filesToOpenOrCreate?: IPathData[]; + readonly filesToDiff?: IPathData[]; } /** * Additional context for the request on native only. */ export interface INativeOpenFileRequest extends IOpenFileRequest { - termProgram?: string; - filesToWait?: IPathsToWaitForData; + readonly termProgram?: string; + readonly filesToWait?: IPathsToWaitForData; } export interface INativeRunActionInWindowRequest { - id: string; - from: 'menu' | 'touchbar' | 'mouse'; - args?: any[]; + readonly id: string; + readonly from: 'menu' | 'touchbar' | 'mouse'; + readonly args?: any[]; } export interface INativeRunKeybindingInWindowRequest { - userSettingsLabel: string; + readonly userSettingsLabel: string; } export interface IColorScheme { - dark: boolean; - highContrast: boolean; + readonly dark: boolean; + readonly highContrast: boolean; } export interface IWindowConfiguration { @@ -225,7 +223,7 @@ export interface IWindowConfiguration { } export interface IOSConfiguration { - release: string; + readonly release: string; } export interface INativeWindowConfiguration extends IWindowConfiguration, NativeParsedArgs { @@ -241,8 +239,7 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native nodeCachedDataDir?: string; partsSplashPath: string; - workspace?: IWorkspaceIdentifier; - folderUri?: ISingleFolderWorkspaceIdentifier; + workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; isInitialStartup?: boolean; logLevel: LogLevel; @@ -250,7 +247,7 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native fullscreen?: boolean; maximized?: boolean; accessibilitySupport?: boolean; - perfEntries: ExportData; + perfMarks: PerformanceMark[]; userEnv: IProcessEnvironment; filesToWait?: IPathsToWaitFor; diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 83eccca7..8bd10465 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -4,18 +4,38 @@ *--------------------------------------------------------------------------------------------*/ import { IWindowOpenable, IOpenEmptyWindowOptions, INativeWindowConfiguration } from 'vs/platform/windows/common/windows'; -import { OpenContext } from 'vs/platform/windows/node/window'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IProcessEnvironment } from 'vs/base/common/platform'; -import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { URI } from 'vs/base/common/uri'; import { Rectangle, BrowserWindow, WebContents } from 'electron'; import { IDisposable } from 'vs/base/common/lifecycle'; import { CancellationToken } from 'vs/base/common/cancellation'; +export const enum OpenContext { + + // opening when running from the command line + CLI, + + // macOS only: opening from the dock (also when opening files to a running instance from desktop) + DOCK, + + // opening from the main application window + MENU, + + // opening from a file or folder dialog + DIALOG, + + // opening from the OS's UI + DESKTOP, + + // opening through the API + API +} + export interface IWindowState { width?: number; height?: number; @@ -45,8 +65,8 @@ export interface ICodeWindow extends IDisposable { readonly win: BrowserWindow; readonly config: INativeWindowConfiguration | undefined; - readonly openedFolderUri?: URI; - readonly openedWorkspace?: IWorkspaceIdentifier; + readonly openedWorkspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; + readonly backupPath?: string; readonly remoteAuthority?: string; @@ -64,7 +84,7 @@ export interface ICodeWindow extends IDisposable { addTabbedWindow(window: ICodeWindow): void; - load(config: INativeWindowConfiguration, isReload?: boolean): void; + load(config: INativeWindowConfiguration, options?: { isReload?: boolean }): void; reload(cli?: NativeParsedArgs): void; focus(options?: { force: boolean }): void; @@ -104,9 +124,11 @@ export interface IWindowsMainService { readonly _serviceBrand: undefined; + readonly onWindowsCountChanged: Event; + readonly onWindowOpened: Event; readonly onWindowReady: Event; - readonly onWindowsCountChanged: Event; + readonly onWindowDestroyed: Event; open(openConfig: IOpenConfiguration): ICodeWindow[]; openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): ICodeWindow[]; @@ -115,13 +137,14 @@ export interface IWindowsMainService { sendToFocused(channel: string, ...args: any[]): void; sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void; + getWindows(): ICodeWindow[]; + getWindowCount(): number; + getFocusedWindow(): ICodeWindow | undefined; getLastActiveWindow(): ICodeWindow | undefined; getWindowById(windowId: number): ICodeWindow | undefined; getWindowByWebContents(webContents: WebContents): ICodeWindow | undefined; - getWindows(): ICodeWindow[]; - getWindowCount(): number; } export interface IBaseOpenConfiguration { diff --git a/src/vs/platform/windows/electron-main/windowsFinder.ts b/src/vs/platform/windows/electron-main/windowsFinder.ts new file mode 100644 index 00000000..30f76018 --- /dev/null +++ b/src/vs/platform/windows/electron-main/windowsFinder.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { IWorkspaceIdentifier, IResolvedWorkspace, isWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; + +export function findWindowOnFile(windows: ICodeWindow[], fileUri: URI, localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | null): ICodeWindow | undefined { + + // First check for windows with workspaces that have a parent folder of the provided path opened + for (const window of windows) { + const workspace = window.openedWorkspace; + if (isWorkspaceIdentifier(workspace)) { + const resolvedWorkspace = localWorkspaceResolver(workspace); + + // resolved workspace: folders are known and can be compared with + if (resolvedWorkspace) { + if (resolvedWorkspace.folders.some(folder => extUriBiasedIgnorePathCase.isEqualOrParent(fileUri, folder.uri))) { + return window; + } + } + + // unresolved: can only compare with workspace location + else { + if (extUriBiasedIgnorePathCase.isEqualOrParent(fileUri, workspace.configPath)) { + return window; + } + } + } + } + + // Then go with single folder windows that are parent of the provided file path + const singleFolderWindowsOnFilePath = windows.filter(window => isSingleFolderWorkspaceIdentifier(window.openedWorkspace) && extUriBiasedIgnorePathCase.isEqualOrParent(fileUri, window.openedWorkspace.uri)); + if (singleFolderWindowsOnFilePath.length) { + return singleFolderWindowsOnFilePath.sort((windowA, windowB) => -((windowA.openedWorkspace as ISingleFolderWorkspaceIdentifier).uri.path.length - (windowB.openedWorkspace as ISingleFolderWorkspaceIdentifier).uri.path.length))[0]; + } + + return undefined; +} + +export function findWindowOnWorkspaceOrFolder(windows: ICodeWindow[], folderOrWorkspaceConfigUri: URI): ICodeWindow | undefined { + + for (const window of windows) { + + // check for workspace config path + if (isWorkspaceIdentifier(window.openedWorkspace) && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.configPath, folderOrWorkspaceConfigUri)) { + return window; + } + + // check for folder path + if (isSingleFolderWorkspaceIdentifier(window.openedWorkspace) && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.uri, folderOrWorkspaceConfigUri)) { + return window; + } + } + + return undefined; +} + + +export function findWindowOnExtensionDevelopmentPath(windows: ICodeWindow[], extensionDevelopmentPaths: string[]): ICodeWindow | undefined { + + const matches = (uriString: string): boolean => { + return extensionDevelopmentPaths.some(path => extUriBiasedIgnorePathCase.isEqual(URI.file(path), URI.file(uriString))); + }; + + for (const window of windows) { + + // match on extension development path. the path can be one or more paths + // so we check if any of the paths match on any of the provided ones + if (window.config?.extensionDevelopmentPath?.some(path => matches(path))) { + return window; + } + } + + return undefined; +} diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 6bcedb5c..eebedf00 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { statSync, unlink } from 'fs'; +import { statSync } from 'fs'; import { basename, normalize, join, posix } from 'vs/base/common/path'; import { localize } from 'vs/nls'; import { coalesce, distinct, firstOrDefault } from 'vs/base/common/arrays'; @@ -12,166 +12,126 @@ import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { IStateService } from 'vs/platform/state/node/state'; -import { CodeWindow, defaultWindowState } from 'vs/code/electron-main/window'; -import { screen, BrowserWindow, MessageBoxOptions, Display, app, WebContents } from 'electron'; +import { CodeWindow } from 'vs/code/electron-main/window'; +import { BrowserWindow, MessageBoxOptions, WebContents } from 'electron'; import { ILifecycleMainService, UnloadReason, LifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; -import { IWindowSettings, IPath, isFileToOpen, isWorkspaceToOpen, isFolderToOpen, IWindowOpenable, IOpenEmptyWindowOptions, IAddFoldersRequest, IPathsToWaitFor, INativeWindowConfiguration } from 'vs/platform/windows/common/windows'; -import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri, OpenContext } from 'vs/platform/windows/node/window'; +import { IWindowSettings, IPath, isFileToOpen, isWorkspaceToOpen, isFolderToOpen, IWindowOpenable, IOpenEmptyWindowOptions, IAddFoldersRequest, IPathsToWaitFor, INativeWindowConfiguration, INativeOpenFileRequest } from 'vs/platform/windows/common/windows'; +import { findWindowOnFile, findWindowOnWorkspaceOrFolder, findWindowOnExtensionDevelopmentPath } from 'vs/platform/windows/electron-main/windowsFinder'; import { Emitter } from 'vs/base/common/event'; import product from 'vs/platform/product/common/product'; -import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode, IOpenEmptyConfiguration } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IOpenEmptyConfiguration, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; -import { IProcessEnvironment, isMacintosh, isWindows } from 'vs/base/common/platform'; -import { IWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, hasWorkspaceFileExtension, IRecent } from 'vs/platform/workspaces/common/workspaces'; +import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; +import { IWorkspaceIdentifier, hasWorkspaceFileExtension, IRecent, isWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { normalizePath, originalFSPath, removeTrailingPathSeparator, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; -import { restoreWindowsState, WindowsStateStorageData, getWindowsStateStoreData } from 'vs/platform/windows/electron-main/windowsStateStorage'; -import { getWorkspaceIdentifier, IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; +import { IWindowState, WindowsStateHandler } from 'vs/platform/windows/electron-main/windowsStateHandler'; +import { getSingleFolderWorkspaceIdentifier, getWorkspaceIdentifier, IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { once } from 'vs/base/common/functional'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { isWindowsDriveLetter, toSlashes, parseLineAndColumnAware } from 'vs/base/common/extpath'; +import { isWindowsDriveLetter, toSlashes, parseLineAndColumnAware, sanitizeFilePath } from 'vs/base/common/extpath'; import { CharCode } from 'vs/base/common/charCode'; import { getPathLabel } from 'vs/base/common/labels'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IFileService } from 'vs/platform/files/common/files'; -export interface IWindowState { - workspace?: IWorkspaceIdentifier; - folderUri?: URI; - backupPath?: string; - remoteAuthority?: string; - uiState: ISingleWindowState; -} - -export interface IWindowsState { - lastActiveWindow?: IWindowState; - lastPluginDevelopmentHostWindow?: IWindowState; - openedWindows: IWindowState[]; -} - -interface INewWindowState extends ISingleWindowState { - hasDefaultState?: boolean; -} +//#region Helper Interfaces type RestoreWindowsSetting = 'preserve' | 'all' | 'folders' | 'one' | 'none'; interface IOpenBrowserWindowOptions { - userEnv?: IProcessEnvironment; - cli?: NativeParsedArgs; + readonly userEnv?: IProcessEnvironment; + readonly cli?: NativeParsedArgs; - workspace?: IWorkspaceIdentifier; - folderUri?: URI; + readonly workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; - remoteAuthority?: string; + readonly remoteAuthority?: string; - initialStartup?: boolean; + readonly initialStartup?: boolean; - filesToOpen?: IFilesToOpen; + readonly filesToOpen?: IFilesToOpen; - forceNewWindow?: boolean; - forceNewTabbedWindow?: boolean; - windowToUse?: ICodeWindow; + readonly forceNewWindow?: boolean; + readonly forceNewTabbedWindow?: boolean; + readonly windowToUse?: ICodeWindow; - emptyWindowBackupInfo?: IEmptyWindowBackupInfo; + readonly emptyWindowBackupInfo?: IEmptyWindowBackupInfo; } -interface IPathParseOptions { - ignoreFileNotFound?: boolean; - gotoLineMode?: boolean; - remoteAuthority?: string; +interface IPathResolveOptions { + readonly ignoreFileNotFound?: boolean; + readonly gotoLineMode?: boolean; + readonly remoteAuthority?: string; } interface IFilesToOpen { + readonly remoteAuthority?: string; + filesToOpenOrCreate: IPath[]; filesToDiff: IPath[]; filesToWait?: IPathsToWaitFor; - remoteAuthority?: string; } interface IPathToOpen extends IPath { - // the workspace for a Code instance to open - workspace?: IWorkspaceIdentifier; + // the workspace to open + readonly workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; - // the folder path for a Code instance to open - folderUri?: URI; - - // the backup path for a Code instance to use - backupPath?: string; + // the backup path to use + readonly backupPath?: string; // the remote authority for the Code instance to open. Undefined if not remote. - remoteAuthority?: string; + readonly remoteAuthority?: string; // optional label for the recent history label?: string; } -function isFolderPathToOpen(path: IPathToOpen): path is IFolderPathToOpen { - return !!path.folderUri; +interface IWorkspacePathToOpen extends IPathToOpen { + readonly workspace: IWorkspaceIdentifier; } -interface IFolderPathToOpen { - - // the folder path for a Code instance to open - folderUri: URI; - - // the backup path for a Code instance to use - backupPath?: string; - - // the remote authority for the Code instance to open. Undefined if not remote. - remoteAuthority?: string; - - // optional label for the recent history - label?: string; +interface ISingleFolderWorkspacePathToOpen extends IPathToOpen { + readonly workspace: ISingleFolderWorkspaceIdentifier; } -function isWorkspacePathToOpen(path: IPathToOpen): path is IWorkspacePathToOpen { - return !!path.workspace; +function isWorkspacePathToOpen(path: IPathToOpen | undefined): path is IWorkspacePathToOpen { + return isWorkspaceIdentifier(path?.workspace); } -interface IWorkspacePathToOpen { - - // the workspace for a Code instance to open - workspace: IWorkspaceIdentifier; - - // the backup path for a Code instance to use - backupPath?: string; - - // the remote authority for the Code instance to open. Undefined if not remote. - remoteAuthority?: string; - - // optional label for the recent history - label?: string; +function isSingleFolderWorkspacePathToOpen(path: IPathToOpen | undefined): path is ISingleFolderWorkspacePathToOpen { + return isSingleFolderWorkspaceIdentifier(path?.workspace); } +//#endregion + export class WindowsMainService extends Disposable implements IWindowsMainService { declare readonly _serviceBrand: undefined; - private static readonly windowsStateStorageKey = 'windowsState'; - private static readonly WINDOWS: ICodeWindow[] = []; - private readonly windowsState: IWindowsState; - private lastClosedWindowState?: IWindowState; - - private shuttingDown = false; - private readonly _onWindowOpened = this._register(new Emitter()); readonly onWindowOpened = this._onWindowOpened.event; private readonly _onWindowReady = this._register(new Emitter()); readonly onWindowReady = this._onWindowReady.event; + private readonly _onWindowDestroyed = this._register(new Emitter()); + readonly onWindowDestroyed = this._onWindowDestroyed.event; + private readonly _onWindowsCountChanged = this._register(new Emitter()); readonly onWindowsCountChanged = this._onWindowsCountChanged.event; + private readonly windowsStateHandler = this._register(new WindowsStateHandler(this, this.stateService, this.lifecycleMainService, this.logService, this.configurationService)); + constructor( private readonly machineId: string, private readonly initialUserEnv: IProcessEnvironment, @@ -182,190 +142,20 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic @IBackupMainService private readonly backupMainService: IBackupMainService, @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService, - @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, + @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IDialogMainService private readonly dialogMainService: IDialogMainService + @IDialogMainService private readonly dialogMainService: IDialogMainService, + @IFileService private readonly fileService: IFileService ) { super(); - this.windowsState = restoreWindowsState(this.stateService.getItem(WindowsMainService.windowsStateStorageKey)); - if (!Array.isArray(this.windowsState.openedWindows)) { - this.windowsState.openedWindows = []; - } - this.lifecycleMainService.when(LifecycleMainPhase.Ready).then(() => this.registerListeners()); - this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => this.installWindowsMutex()); - } - - private installWindowsMutex(): void { - const win32MutexName = product.win32MutexName; - if (isWindows && win32MutexName) { - try { - const WindowsMutex = (require.__$__nodeRequire('windows-mutex') as typeof import('windows-mutex')).Mutex; - const mutex = new WindowsMutex(win32MutexName); - once(this.lifecycleMainService.onWillShutdown)(() => mutex.release()); - } catch (e) { - this.logService.error(e); - } - } } private registerListeners(): void { - // When a window looses focus, save all windows state. This allows to - // prevent loss of window-state data when OS is restarted without properly - // shutting down the application (https://github.com/microsoft/vscode/issues/87171) - app.on('browser-window-blur', () => { - if (!this.shuttingDown) { - this.saveWindowsState(); - } - }); - - // Handle various lifecycle events around windows - this.lifecycleMainService.onBeforeWindowClose(window => this.onBeforeWindowClose(window)); - this.lifecycleMainService.onBeforeShutdown(() => this.onBeforeShutdown()); - this.onWindowsCountChanged(e => { - if (e.newCount - e.oldCount > 0) { - // clear last closed window state when a new window opens. this helps on macOS where - // otherwise closing the last window, opening a new window and then quitting would - // use the state of the previously closed window when restarting. - this.lastClosedWindowState = undefined; - } - }); - // Signal a window is ready after having entered a workspace - this._register(this.workspacesMainService.onWorkspaceEntered(event => { - this._onWindowReady.fire(event.window); - })); - } - - // Note that onBeforeShutdown() and onBeforeWindowClose() are fired in different order depending on the OS: - // - macOS: since the app will not quit when closing the last window, you will always first get - // the onBeforeShutdown() event followed by N onBeforeWindowClose() events for each window - // - other: on other OS, closing the last window will quit the app so the order depends on the - // user interaction: closing the last window will first trigger onBeforeWindowClose() - // and then onBeforeShutdown(). Using the quit action however will first issue onBeforeShutdown() - // and then onBeforeWindowClose(). - // - // Here is the behavior on different OS depending on action taken (Electron 1.7.x): - // - // Legend - // - quit(N): quit application with N windows opened - // - close(1): close one window via the window close button - // - closeAll: close all windows via the taskbar command - // - onBeforeShutdown(N): number of windows reported in this event handler - // - onBeforeWindowClose(N, M): number of windows reported and quitRequested boolean in this event handler - // - // macOS - // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) - // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) - // - quit(0): onBeforeShutdown(0) - // - close(1): onBeforeWindowClose(1, false) - // - // Windows - // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) - // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) - // - close(1): onBeforeWindowClose(2, false)[not last window] - // - close(1): onBeforeWindowClose(1, false), onBeforeShutdown(0)[last window] - // - closeAll(2): onBeforeWindowClose(2, false), onBeforeWindowClose(2, false), onBeforeShutdown(0) - // - // Linux - // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) - // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) - // - close(1): onBeforeWindowClose(2, false)[not last window] - // - close(1): onBeforeWindowClose(1, false), onBeforeShutdown(0)[last window] - // - closeAll(2): onBeforeWindowClose(2, false), onBeforeWindowClose(2, false), onBeforeShutdown(0) - // - private onBeforeShutdown(): void { - this.shuttingDown = true; - - this.saveWindowsState(); - } - - private saveWindowsState(): void { - const currentWindowsState: IWindowsState = { - openedWindows: [], - lastPluginDevelopmentHostWindow: this.windowsState.lastPluginDevelopmentHostWindow, - lastActiveWindow: this.lastClosedWindowState - }; - - // 1.) Find a last active window (pick any other first window otherwise) - if (!currentWindowsState.lastActiveWindow) { - let activeWindow = this.getLastActiveWindow(); - if (!activeWindow || activeWindow.isExtensionDevelopmentHost) { - activeWindow = WindowsMainService.WINDOWS.find(window => !window.isExtensionDevelopmentHost); - } - - if (activeWindow) { - currentWindowsState.lastActiveWindow = this.toWindowState(activeWindow); - } - } - - // 2.) Find extension host window - const extensionHostWindow = WindowsMainService.WINDOWS.find(window => window.isExtensionDevelopmentHost && !window.isExtensionTestHost); - if (extensionHostWindow) { - currentWindowsState.lastPluginDevelopmentHostWindow = this.toWindowState(extensionHostWindow); - } - - // 3.) All windows (except extension host) for N >= 2 to support `restoreWindows: all` or for auto update - // - // Careful here: asking a window for its window state after it has been closed returns bogus values (width: 0, height: 0) - // so if we ever want to persist the UI state of the last closed window (window count === 1), it has - // to come from the stored lastClosedWindowState on Win/Linux at least - if (this.getWindowCount() > 1) { - currentWindowsState.openedWindows = WindowsMainService.WINDOWS.filter(window => !window.isExtensionDevelopmentHost).map(window => this.toWindowState(window)); - } - - // Persist - const state = getWindowsStateStoreData(currentWindowsState); - this.stateService.setItem(WindowsMainService.windowsStateStorageKey, state); - - if (this.shuttingDown) { - this.logService.trace('onBeforeShutdown', state); - } - } - - // See note on #onBeforeShutdown() for details how these events are flowing - private onBeforeWindowClose(win: ICodeWindow): void { - if (this.lifecycleMainService.quitRequested) { - return; // during quit, many windows close in parallel so let it be handled in the before-quit handler - } - - // On Window close, update our stored UI state of this window - const state: IWindowState = this.toWindowState(win); - if (win.isExtensionDevelopmentHost && !win.isExtensionTestHost) { - this.windowsState.lastPluginDevelopmentHostWindow = state; // do not let test run window state overwrite our extension development state - } - - // Any non extension host window with same workspace or folder - else if (!win.isExtensionDevelopmentHost && (!!win.openedWorkspace || !!win.openedFolderUri)) { - this.windowsState.openedWindows.forEach(o => { - const sameWorkspace = win.openedWorkspace && o.workspace && o.workspace.id === win.openedWorkspace.id; - const sameFolder = win.openedFolderUri && o.folderUri && extUriBiasedIgnorePathCase.isEqual(o.folderUri, win.openedFolderUri); - - if (sameWorkspace || sameFolder) { - o.uiState = state.uiState; - } - }); - } - - // On Windows and Linux closing the last window will trigger quit. Since we are storing all UI state - // before quitting, we need to remember the UI state of this window to be able to persist it. - // On macOS we keep the last closed window state ready in case the user wants to quit right after or - // wants to open another window, in which case we use this state over the persisted one. - if (this.getWindowCount() === 1) { - this.lastClosedWindowState = state; - } - } - - private toWindowState(win: ICodeWindow): IWindowState { - return { - workspace: win.openedWorkspace, - folderUri: win.openedFolderUri, - backupPath: win.backupPath, - remoteAuthority: win.remoteAuthority, - uiState: win.serializeWindowState() - }; + this._register(this.workspacesManagementMainService.onWorkspaceEntered(event => this._onWindowReady.fire(event.window))); } openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): ICodeWindow[] { @@ -375,18 +165,22 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic cli = { ...cli, remote }; } + const forceEmpty = true; const forceReuseWindow = options?.forceReuseWindow; const forceNewWindow = !forceReuseWindow; - return this.open({ ...openConfig, cli, forceEmpty: true, forceNewWindow, forceReuseWindow }); + return this.open({ ...openConfig, cli, forceEmpty, forceNewWindow, forceReuseWindow }); } open(openConfig: IOpenConfiguration): ICodeWindow[] { this.logService.trace('windowsManager#open'); - openConfig = this.validateOpenConfig(openConfig); - const foldersToAdd: IFolderPathToOpen[] = []; - const foldersToOpen: IFolderPathToOpen[] = []; + if (openConfig.addMode && (openConfig.initialStartup || !this.getLastActiveWindow())) { + openConfig.addMode = false; // Make sure addMode is only enabled if we have an active window + } + + const foldersToAdd: ISingleFolderWorkspacePathToOpen[] = []; + const foldersToOpen: ISingleFolderWorkspacePathToOpen[] = []; const workspacesToOpen: IWorkspacePathToOpen[] = []; const workspacesToRestore: IWorkspacePathToOpen[] = []; const emptyToRestore: IEmptyWindowBackupInfo[] = []; @@ -397,7 +191,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const pathsToOpen = this.getPathsToOpen(openConfig); this.logService.trace('windowsManager#open pathsToOpen', pathsToOpen); for (const path of pathsToOpen) { - if (isFolderPathToOpen(path)) { + if (isSingleFolderWorkspacePathToOpen(path)) { if (openConfig.addMode) { // When run with --add, take the folders that are to be opened as // folders that should be added to the currently active window. @@ -431,13 +225,11 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic filesToOpen.filesToWait = { paths: [...filesToOpen.filesToDiff, ...filesToOpen.filesToOpenOrCreate], waitMarkerFileUri: openConfig.waitMarkerFileURI }; } - // // These are windows to restore because of hot-exit or from previous session (only performed once on startup!) - // if (openConfig.initialStartup) { // Untitled workspaces are always restored - workspacesToRestore.push(...this.workspacesMainService.getUntitledWorkspacesSync()); + workspacesToRestore.push(...this.workspacesManagementMainService.getUntitledWorkspacesSync()); workspacesToOpen.push(...workspacesToRestore); // Empty windows with backups are always restored @@ -461,13 +253,13 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Otherwise, find a good window based on open params else { - const focusLastActive = this.windowsState.lastActiveWindow && !openConfig.forceEmpty && !openConfig.cli._.length && !openConfig.cli['file-uri'] && !openConfig.cli['folder-uri'] && !(openConfig.urisToOpen && openConfig.urisToOpen.length); + const focusLastActive = this.windowsStateHandler.state.lastActiveWindow && !openConfig.forceEmpty && !openConfig.cli._.length && !openConfig.cli['file-uri'] && !openConfig.cli['folder-uri'] && !(openConfig.urisToOpen && openConfig.urisToOpen.length); let focusLastOpened = true; let focusLastWindow = true; // 2.) focus last active window if we are not instructed to open any paths if (focusLastActive) { - const lastActiveWindow = usedWindows.filter(window => this.windowsState.lastActiveWindow && window.backupPath === this.windowsState.lastActiveWindow.backupPath); + const lastActiveWindow = usedWindows.filter(window => this.windowsStateHandler.state.lastActiveWindow && window.backupPath === this.windowsStateHandler.state.lastActiveWindow.backupPath); if (lastActiveWindow.length) { lastActiveWindow[0].focus(); focusLastOpened = false; @@ -504,11 +296,11 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const isDiff = filesToOpen && filesToOpen.filesToDiff.length > 0; if (!usedWindows.some(window => window.isExtensionDevelopmentHost) && !isDiff && !openConfig.noRecentEntry) { const recents: IRecent[] = []; - for (let pathToOpen of pathsToOpen) { - if (pathToOpen.workspace) { + for (const pathToOpen of pathsToOpen) { + if (isWorkspacePathToOpen(pathToOpen)) { recents.push({ label: pathToOpen.label, workspace: pathToOpen.workspace }); - } else if (pathToOpen.folderUri) { - recents.push({ label: pathToOpen.label, folderUri: pathToOpen.folderUri }); + } else if (isSingleFolderWorkspacePathToOpen(pathToOpen)) { + recents.push({ label: pathToOpen.label, folderUri: pathToOpen.workspace.uri }); } else if (pathToOpen.fileUri) { recents.push({ label: pathToOpen.label, fileUri: pathToOpen.fileUri }); } @@ -522,33 +314,34 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // process can continue. We do this by deleting the waitMarkerFilePath. const waitMarkerFileURI = openConfig.waitMarkerFileURI; if (openConfig.context === OpenContext.CLI && waitMarkerFileURI && usedWindows.length === 1 && usedWindows[0]) { - usedWindows[0].whenClosedOrLoaded.then(() => unlink(waitMarkerFileURI.fsPath, () => undefined)); + usedWindows[0].whenClosedOrLoaded.then(() => this.fileService.del(waitMarkerFileURI), () => undefined); } return usedWindows; } - private validateOpenConfig(config: IOpenConfiguration): IOpenConfiguration { - - // Make sure addMode is only enabled if we have an active window - if (config.addMode && (config.initialStartup || !this.getLastActiveWindow())) { - config.addMode = false; - } - - return config; - } - private doOpen( openConfig: IOpenConfiguration, workspacesToOpen: IWorkspacePathToOpen[], - foldersToOpen: IFolderPathToOpen[], + foldersToOpen: ISingleFolderWorkspacePathToOpen[], emptyToRestore: IEmptyWindowBackupInfo[], emptyToOpen: number, filesToOpen: IFilesToOpen | undefined, - foldersToAdd: IFolderPathToOpen[] + foldersToAdd: ISingleFolderWorkspacePathToOpen[] ): { windows: ICodeWindow[], filesOpenedInWindow: ICodeWindow | undefined } { + + // Keep track of used windows and remember + // if files have been opened in one of them const usedWindows: ICodeWindow[] = []; let filesOpenedInWindow: ICodeWindow | undefined = undefined; + function addUsedWindow(window: ICodeWindow, openedFiles?: boolean): void { + usedWindows.push(window); + + if (openedFiles) { + filesOpenedInWindow = window; + filesToOpen = undefined; // reset `filesToOpen` since files have been opened + } + } // Settings can decide if files/folders open in new window or not let { openFolderInNewWindow, openFilesInNewWindow } = this.shouldOpenNewWindow(openConfig); @@ -558,57 +351,59 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const authority = foldersToAdd[0].remoteAuthority; const lastActiveWindow = this.getLastActiveWindowForAuthority(authority); if (lastActiveWindow) { - usedWindows.push(this.doAddFoldersToExistingWindow(lastActiveWindow, foldersToAdd.map(f => f.folderUri))); + addUsedWindow(this.doAddFoldersToExistingWindow(lastActiveWindow, foldersToAdd.map(folderToAdd => folderToAdd.workspace.uri))); } } - // Handle files to open/diff or to create when we dont open a folder and we do not restore any folder/untitled from hot-exit - const potentialWindowsCount = foldersToOpen.length + workspacesToOpen.length + emptyToRestore.length; - if (potentialWindowsCount === 0 && filesToOpen) { + // Handle files to open/diff or to create when we dont open a folder and we do not restore any + // folder/untitled from hot-exit by trying to open them in the window that fits best + const potentialNewWindowsCount = foldersToOpen.length + workspacesToOpen.length + emptyToRestore.length; + if (filesToOpen && potentialNewWindowsCount === 0) { // Find suitable window or folder path to open files in const fileToCheck = filesToOpen.filesToOpenOrCreate[0] || filesToOpen.filesToDiff[0]; // only look at the windows with correct authority - const windows = WindowsMainService.WINDOWS.filter(window => filesToOpen && window.remoteAuthority === filesToOpen.remoteAuthority); + const windows = this.getWindows().filter(window => filesToOpen && window.remoteAuthority === filesToOpen.remoteAuthority); - const bestWindowOrFolder = findBestWindowOrFolderForFile({ - windows, - newWindow: openFilesInNewWindow, - context: openConfig.context, - fileUri: fileToCheck?.fileUri, - localWorkspaceResolver: workspace => workspace.configPath.scheme === Schemas.file ? this.workspacesMainService.resolveLocalWorkspaceSync(workspace.configPath) : null - }); + // figure out a good window to open the files in if any + // with a fallback to the last active window. + // + // in case `openFilesInNewWindow` is enforced, we skip + // this step. + let windowToUseForFiles: ICodeWindow | undefined = undefined; + if (fileToCheck?.fileUri && !openFilesInNewWindow) { + if (openConfig.context === OpenContext.DESKTOP || openConfig.context === OpenContext.CLI || openConfig.context === OpenContext.DOCK) { + windowToUseForFiles = findWindowOnFile(windows, fileToCheck.fileUri, workspace => workspace.configPath.scheme === Schemas.file ? this.workspacesManagementMainService.resolveLocalWorkspaceSync(workspace.configPath) : null); + } + + if (!windowToUseForFiles) { + windowToUseForFiles = this.doGetLastActiveWindow(windows); + } + } // We found a window to open the files in - if (bestWindowOrFolder instanceof CodeWindow) { + if (windowToUseForFiles) { // Window is workspace - if (bestWindowOrFolder.openedWorkspace) { - workspacesToOpen.push({ workspace: bestWindowOrFolder.openedWorkspace, remoteAuthority: bestWindowOrFolder.remoteAuthority }); + if (isWorkspaceIdentifier(windowToUseForFiles.openedWorkspace)) { + workspacesToOpen.push({ workspace: windowToUseForFiles.openedWorkspace, remoteAuthority: windowToUseForFiles.remoteAuthority }); } // Window is single folder - else if (bestWindowOrFolder.openedFolderUri) { - foldersToOpen.push({ folderUri: bestWindowOrFolder.openedFolderUri, remoteAuthority: bestWindowOrFolder.remoteAuthority }); + else if (isSingleFolderWorkspaceIdentifier(windowToUseForFiles.openedWorkspace)) { + foldersToOpen.push({ workspace: windowToUseForFiles.openedWorkspace, remoteAuthority: windowToUseForFiles.remoteAuthority }); } // Window is empty else { - - // Do open files - const window = this.doOpenFilesInExistingWindow(openConfig, bestWindowOrFolder, filesToOpen); - usedWindows.push(window); - - // Reset `filesToOpen` because we handled them and also remember window we used - filesToOpen = undefined; - filesOpenedInWindow = window; + addUsedWindow(this.doOpenFilesInExistingWindow(openConfig, windowToUseForFiles, filesToOpen), true); } } // Finally, if no window or folder is found, just open the files in an empty window else { - const window = this.openInBrowserWindow({ + addUsedWindow(this.openInBrowserWindow({ userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, @@ -616,12 +411,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic forceNewWindow: true, remoteAuthority: filesToOpen.remoteAuthority, forceNewTabbedWindow: openConfig.forceNewTabbedWindow - }); - usedWindows.push(window); - - // Reset `filesToOpen` because we handled them and also remember window we used - filesToOpen = undefined; - filesOpenedInWindow = window; + }), true); } } @@ -630,27 +420,20 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic if (allWorkspacesToOpen.length > 0) { // Check for existing instances - const windowsOnWorkspace = coalesce(allWorkspacesToOpen.map(workspaceToOpen => findWindowOnWorkspace(WindowsMainService.WINDOWS, workspaceToOpen.workspace))); + const windowsOnWorkspace = coalesce(allWorkspacesToOpen.map(workspaceToOpen => findWindowOnWorkspaceOrFolder(this.getWindows(), workspaceToOpen.workspace.configPath))); if (windowsOnWorkspace.length > 0) { const windowOnWorkspace = windowsOnWorkspace[0]; const filesToOpenInWindow = (filesToOpen?.remoteAuthority === windowOnWorkspace.remoteAuthority) ? filesToOpen : undefined; // Do open files - const window = this.doOpenFilesInExistingWindow(openConfig, windowOnWorkspace, filesToOpenInWindow); - usedWindows.push(window); - - // Reset `filesToOpen` because we handled them and also remember window we used - if (filesToOpenInWindow) { - filesToOpen = undefined; - filesOpenedInWindow = window; - } + addUsedWindow(this.doOpenFilesInExistingWindow(openConfig, windowOnWorkspace, filesToOpenInWindow), !!filesToOpenInWindow); openFolderInNewWindow = true; // any other folders to open must open in new window then } // Open remaining ones allWorkspacesToOpen.forEach(workspaceToOpen => { - if (windowsOnWorkspace.some(win => win.openedWorkspace && win.openedWorkspace.id === workspaceToOpen.workspace.id)) { + if (windowsOnWorkspace.some(window => window.openedWorkspace && window.openedWorkspace.id === workspaceToOpen.workspace.id)) { return; // ignore folders that are already open } @@ -658,46 +441,31 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const filesToOpenInWindow = (filesToOpen?.remoteAuthority === remoteAuthority) ? filesToOpen : undefined; // Do open folder - const window = this.doOpenFolderOrWorkspace(openConfig, workspaceToOpen, openFolderInNewWindow, filesToOpenInWindow); - usedWindows.push(window); - - // Reset `filesToOpen` because we handled them and also remember window we used - if (filesToOpenInWindow) { - filesToOpen = undefined; - filesOpenedInWindow = window; - } + addUsedWindow(this.doOpenFolderOrWorkspace(openConfig, workspaceToOpen, openFolderInNewWindow, filesToOpenInWindow), !!filesToOpenInWindow); openFolderInNewWindow = true; // any other folders to open must open in new window then }); } // Handle folders to open (instructed and to restore) - const allFoldersToOpen = distinct(foldersToOpen, folder => extUriBiasedIgnorePathCase.getComparisonKey(folder.folderUri)); // prevent duplicates + const allFoldersToOpen = distinct(foldersToOpen, folder => extUriBiasedIgnorePathCase.getComparisonKey(folder.workspace.uri)); // prevent duplicates if (allFoldersToOpen.length > 0) { // Check for existing instances - const windowsOnFolderPath = coalesce(allFoldersToOpen.map(folderToOpen => findWindowOnWorkspace(WindowsMainService.WINDOWS, folderToOpen.folderUri))); + const windowsOnFolderPath = coalesce(allFoldersToOpen.map(folderToOpen => findWindowOnWorkspaceOrFolder(this.getWindows(), folderToOpen.workspace.uri))); if (windowsOnFolderPath.length > 0) { const windowOnFolderPath = windowsOnFolderPath[0]; const filesToOpenInWindow = filesToOpen?.remoteAuthority === windowOnFolderPath.remoteAuthority ? filesToOpen : undefined; // Do open files - const window = this.doOpenFilesInExistingWindow(openConfig, windowOnFolderPath, filesToOpenInWindow); - usedWindows.push(window); - - // Reset `filesToOpen` because we handled them and also remember window we used - if (filesToOpenInWindow) { - filesToOpen = undefined; - filesOpenedInWindow = window; - } + addUsedWindow(this.doOpenFilesInExistingWindow(openConfig, windowOnFolderPath, filesToOpenInWindow), !!filesToOpenInWindow); openFolderInNewWindow = true; // any other folders to open must open in new window then } // Open remaining ones allFoldersToOpen.forEach(folderToOpen => { - - if (windowsOnFolderPath.some(win => extUriBiasedIgnorePathCase.isEqual(win.openedFolderUri, folderToOpen.folderUri))) { + if (windowsOnFolderPath.some(window => isSingleFolderWorkspaceIdentifier(window.openedWorkspace) && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.uri, folderToOpen.workspace.uri))) { return; // ignore folders that are already open } @@ -705,14 +473,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const filesToOpenInWindow = (filesToOpen?.remoteAuthority === remoteAuthority) ? filesToOpen : undefined; // Do open folder - const window = this.doOpenFolderOrWorkspace(openConfig, folderToOpen, openFolderInNewWindow, filesToOpenInWindow); - usedWindows.push(window); - - // Reset `filesToOpen` because we handled them and also remember window we used - if (filesToOpenInWindow) { - filesToOpen = undefined; - filesOpenedInWindow = window; - } + addUsedWindow(this.doOpenFolderOrWorkspace(openConfig, folderToOpen, openFolderInNewWindow, filesToOpenInWindow), !!filesToOpenInWindow); openFolderInNewWindow = true; // any other folders to open must open in new window then }); @@ -725,7 +486,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const remoteAuthority = emptyWindowBackupInfo.remoteAuthority; const filesToOpenInWindow = (filesToOpen?.remoteAuthority === remoteAuthority) ? filesToOpen : undefined; - const window = this.openInBrowserWindow({ + addUsedWindow(this.openInBrowserWindow({ userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, @@ -734,14 +495,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic forceNewWindow: true, forceNewTabbedWindow: openConfig.forceNewTabbedWindow, emptyWindowBackupInfo - }); - usedWindows.push(window); - - // Reset `filesToOpen` because we handled them and also remember window we used - if (filesToOpenInWindow) { - filesToOpen = undefined; - filesOpenedInWindow = window; - } + }), !!filesToOpenInWindow); openFolderInNewWindow = true; // any other folders to open must open in new window then }); @@ -756,14 +510,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const remoteAuthority = filesToOpen ? filesToOpen.remoteAuthority : (openConfig.cli && openConfig.cli.remote || undefined); for (let i = 0; i < emptyToOpen; i++) { - const window = this.doOpenEmpty(openConfig, openFolderInNewWindow, remoteAuthority, filesToOpen); - usedWindows.push(window); - - // Reset `filesToOpen` because we handled them and also remember window we used - if (filesToOpen) { - filesToOpen = undefined; - filesOpenedInWindow = window; - } + addUsedWindow(this.doOpenEmpty(openConfig, openFolderInNewWindow, remoteAuthority, filesToOpen), !!filesToOpen); // any other window to open must open in new window then openFolderInNewWindow = true; @@ -778,23 +525,20 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic window.focus(); // make sure window has focus - const params: { filesToOpenOrCreate?: IPath[], filesToDiff?: IPath[], filesToWait?: IPathsToWaitFor, termProgram?: string } = {}; - if (filesToOpen) { - params.filesToOpenOrCreate = filesToOpen.filesToOpenOrCreate; - params.filesToDiff = filesToOpen.filesToDiff; - params.filesToWait = filesToOpen.filesToWait; - } - - if (configuration.userEnv) { - params.termProgram = configuration.userEnv['TERM_PROGRAM']; - } - + const params: INativeOpenFileRequest = { + filesToOpenOrCreate: filesToOpen?.filesToOpenOrCreate, + filesToDiff: filesToOpen?.filesToDiff, + filesToWait: filesToOpen?.filesToWait, + termProgram: configuration?.userEnv?.['TERM_PROGRAM'] + }; window.sendWhenReady('vscode:openFiles', CancellationToken.None, params); return window; } private doAddFoldersToExistingWindow(window: ICodeWindow, foldersToAdd: URI[]): ICodeWindow { + this.logService.trace('windowsManager#doAddFoldersToExistingWindow'); + window.focus(); // make sure window has focus const request: IAddFoldersRequest = { foldersToAdd }; @@ -820,50 +564,57 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic }); } - private doOpenFolderOrWorkspace(openConfig: IOpenConfiguration, folderOrWorkspace: IPathToOpen, forceNewWindow: boolean, filesToOpen: IFilesToOpen | undefined, windowToUse?: ICodeWindow): ICodeWindow { + private doOpenFolderOrWorkspace(openConfig: IOpenConfiguration, folderOrWorkspace: IWorkspacePathToOpen | ISingleFolderWorkspacePathToOpen, forceNewWindow: boolean, filesToOpen: IFilesToOpen | undefined, windowToUse?: ICodeWindow): ICodeWindow { if (!forceNewWindow && !windowToUse && typeof openConfig.contextWindowId === 'number') { windowToUse = this.getWindowById(openConfig.contextWindowId); // fix for https://github.com/microsoft/vscode/issues/49587 } return this.openInBrowserWindow({ + workspace: folderOrWorkspace.workspace, userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, - workspace: folderOrWorkspace.workspace, - folderUri: folderOrWorkspace.folderUri, - filesToOpen, remoteAuthority: folderOrWorkspace.remoteAuthority, forceNewWindow, forceNewTabbedWindow: openConfig.forceNewTabbedWindow, + filesToOpen, windowToUse }); } private getPathsToOpen(openConfig: IOpenConfiguration): IPathToOpen[] { - let windowsToOpen: IPathToOpen[]; + let pathsToOpen: IPathToOpen[]; let isCommandLineOrAPICall = false; let restoredWindows = false; // Extract paths: from API if (openConfig.urisToOpen && openConfig.urisToOpen.length > 0) { - windowsToOpen = this.doExtractPathsFromAPI(openConfig); + pathsToOpen = this.doExtractPathsFromAPI(openConfig); isCommandLineOrAPICall = true; } // Check for force empty else if (openConfig.forceEmpty) { - windowsToOpen = [Object.create(null)]; + pathsToOpen = [Object.create(null)]; } // Extract paths: from CLI else if (openConfig.cli._.length || openConfig.cli['folder-uri'] || openConfig.cli['file-uri']) { - windowsToOpen = this.doExtractPathsFromCLI(openConfig.cli); + pathsToOpen = this.doExtractPathsFromCLI(openConfig.cli); + if (pathsToOpen.length === 0) { + pathsToOpen.push(Object.create(null)); // add an empty window if we did not have windows to open from command line + } + isCommandLineOrAPICall = true; } - // Extract windows: from previous session + // Extract paths: from previous session else { - windowsToOpen = this.doGetWindowsFromLastSession(); + pathsToOpen = this.doGetPathsFromLastSession(); + if (pathsToOpen.length === 0) { + pathsToOpen.push(Object.create(null)); // add an empty window if we did not have windows to restore + } + restoredWindows = true; } @@ -872,15 +623,15 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // If we are in `addMode`, we should not do this because in that case all // folders should be added to the existing window. if (!openConfig.addMode && isCommandLineOrAPICall) { - const foldersToOpen = windowsToOpen.filter(path => !!path.folderUri); + const foldersToOpen = pathsToOpen.filter(path => isSingleFolderWorkspacePathToOpen(path)) as ISingleFolderWorkspacePathToOpen[]; if (foldersToOpen.length > 1) { const remoteAuthority = foldersToOpen[0].remoteAuthority; - if (foldersToOpen.every(f => f.remoteAuthority === remoteAuthority)) { // only if all folder have the same authority - const workspace = this.workspacesMainService.createUntitledWorkspaceSync(foldersToOpen.map(folder => ({ uri: folder.folderUri! }))); + if (foldersToOpen.every(folderToOpen => folderToOpen.remoteAuthority === remoteAuthority)) { // only if all folder have the same authority + const workspace = this.workspacesManagementMainService.createUntitledWorkspaceSync(foldersToOpen.map(folder => ({ uri: folder.workspace.uri }))); // Add workspace and remove folders thereby - windowsToOpen.push({ workspace, remoteAuthority }); - windowsToOpen = windowsToOpen.filter(path => !path.folderUri); + pathsToOpen.push({ workspace, remoteAuthority }); + pathsToOpen = pathsToOpen.filter(path => !isSingleFolderWorkspacePathToOpen(path)); } } } @@ -891,63 +642,57 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Use `unshift` to ensure any new window to open comes last // for proper focus treatment. if (openConfig.initialStartup && !restoredWindows && this.configurationService.getValue('window')?.restoreWindows === 'preserve') { - windowsToOpen.unshift(...this.doGetWindowsFromLastSession().filter(window => window.workspace || window.folderUri || window.backupPath)); + pathsToOpen.unshift(...this.doGetPathsFromLastSession().filter(path => isWorkspacePathToOpen(path) || isSingleFolderWorkspacePathToOpen(path) || path.backupPath)); } - return windowsToOpen; + return pathsToOpen; } private doExtractPathsFromAPI(openConfig: IOpenConfiguration): IPathToOpen[] { const pathsToOpen: IPathToOpen[] = []; - const parseOptions: IPathParseOptions = { gotoLineMode: openConfig.gotoLineMode }; - for (const pathToOpen of openConfig.urisToOpen || []) { - if (!pathToOpen) { - continue; - } + const pathResolveOptions: IPathResolveOptions = { gotoLineMode: openConfig.gotoLineMode }; + for (const pathToOpen of coalesce(openConfig.urisToOpen || [])) { + const path = this.resolveOpenable(pathToOpen, pathResolveOptions); - const path = this.parseUri(pathToOpen, parseOptions); + // Path exists if (path) { path.label = pathToOpen.label; pathsToOpen.push(path); - } else { - const uri = this.resourceFromURIToOpen(pathToOpen); + } - // Warn about the invalid URI or path - let message, detail; - if (uri.scheme === Schemas.file) { - message = localize('pathNotExistTitle', "Path does not exist"); - detail = localize('pathNotExistDetail', "The path '{0}' does not seem to exist anymore on disk.", getPathLabel(uri.fsPath, this.environmentService)); - } else { - message = localize('uriInvalidTitle', "URI can not be opened"); - detail = localize('uriInvalidDetail', "The URI '{0}' is not valid and can not be opened.", uri.toString()); - } + // Path does not exist: show a warning box + else { + const uri = this.resourceFromOpenable(pathToOpen); const options: MessageBoxOptions = { title: product.nameLong, type: 'info', buttons: [localize('ok', "OK")], - message, - detail, + message: uri.scheme === Schemas.file ? localize('pathNotExistTitle', "Path does not exist") : localize('uriInvalidTitle', "URI can not be opened"), + detail: uri.scheme === Schemas.file ? + localize('pathNotExistDetail', "The path '{0}' does not seem to exist anymore on disk.", getPathLabel(uri.fsPath, this.environmentService)) : + localize('uriInvalidDetail', "The URI '{0}' is not valid and can not be opened.", uri.toString()), noLink: true }; this.dialogMainService.showMessageBox(options, withNullAsUndefined(BrowserWindow.getFocusedWindow())); } } + return pathsToOpen; } private doExtractPathsFromCLI(cli: NativeParsedArgs): IPath[] { const pathsToOpen: IPathToOpen[] = []; - const parseOptions: IPathParseOptions = { ignoreFileNotFound: true, gotoLineMode: cli.goto, remoteAuthority: cli.remote || undefined }; + const pathResolveOptions: IPathResolveOptions = { ignoreFileNotFound: true, gotoLineMode: cli.goto, remoteAuthority: cli.remote || undefined }; // folder uris const folderUris = cli['folder-uri']; if (folderUris) { - for (let f of folderUris) { - const folderUri = this.argToUri(f); + for (const rawFolderUri of folderUris) { + const folderUri = this.cliArgToUri(rawFolderUri); if (folderUri) { - const path = this.parseUri({ folderUri }, parseOptions); + const path = this.resolveOpenable({ folderUri }, pathResolveOptions); if (path) { pathsToOpen.push(path); } @@ -958,10 +703,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // file uris const fileUris = cli['file-uri']; if (fileUris) { - for (let f of fileUris) { - const fileUri = this.argToUri(f); + for (const rawFileUri of fileUris) { + const fileUri = this.cliArgToUri(rawFileUri); if (fileUri) { - const path = this.parseUri(hasWorkspaceFileExtension(f) ? { workspaceUri: fileUri } : { fileUri }, parseOptions); + const path = this.resolveOpenable(hasWorkspaceFileExtension(rawFileUri) ? { workspaceUri: fileUri } : { fileUri }, pathResolveOptions); if (path) { pathsToOpen.push(path); } @@ -970,30 +715,42 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } // folder or file paths - const cliArgs = cli._; - for (let cliArg of cliArgs) { - const path = this.parsePath(cliArg, parseOptions); + const cliPaths = cli._; + for (const cliPath of cliPaths) { + const path = this.doResolveFileOpenable(cliPath, pathResolveOptions); if (path) { pathsToOpen.push(path); } } - if (pathsToOpen.length) { - return pathsToOpen; - } - - // No path provided, return empty to open empty - return [Object.create(null)]; + return pathsToOpen; } - private doGetWindowsFromLastSession(): IPathToOpen[] { + private cliArgToUri(arg: string): URI | undefined { + try { + const uri = URI.parse(arg); + if (!uri.scheme) { + this.logService.error(`Invalid URI input string, scheme missing: ${arg}`); + + return undefined; + } + + return uri; + } catch (e) { + this.logService.error(`Invalid URI input string: ${arg}, ${e.message}`); + } + + return undefined; + } + + private doGetPathsFromLastSession(): IPathToOpen[] { const restoreWindowsSetting = this.getRestoreWindowsSetting(); switch (restoreWindowsSetting) { - // none: we always open an empty window + // none: no window to restore case 'none': - return [Object.create(null)]; + return []; // one: restore last opened workspace/folder or empty window // all: restore all windows @@ -1004,48 +761,41 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic case 'folders': // Collect previously opened windows - const openedWindows: IWindowState[] = []; + const lastSessionWindows: IWindowState[] = []; if (restoreWindowsSetting !== 'one') { - openedWindows.push(...this.windowsState.openedWindows); + lastSessionWindows.push(...this.windowsStateHandler.state.openedWindows); } - if (this.windowsState.lastActiveWindow) { - openedWindows.push(this.windowsState.lastActiveWindow); + if (this.windowsStateHandler.state.lastActiveWindow) { + lastSessionWindows.push(this.windowsStateHandler.state.lastActiveWindow); } - const windowsToOpen: IPathToOpen[] = []; - for (const openedWindow of openedWindows) { + const pathsToOpen: IPathToOpen[] = []; + for (const lastSessionWindow of lastSessionWindows) { // Workspaces - if (openedWindow.workspace) { - const pathToOpen = this.parseUri({ workspaceUri: openedWindow.workspace.configPath }, { remoteAuthority: openedWindow.remoteAuthority }); - if (pathToOpen?.workspace) { - windowsToOpen.push(pathToOpen); + if (lastSessionWindow.workspace) { + const pathToOpen = this.resolveOpenable({ workspaceUri: lastSessionWindow.workspace.configPath }, { remoteAuthority: lastSessionWindow.remoteAuthority }); + if (isWorkspacePathToOpen(pathToOpen)) { + pathsToOpen.push(pathToOpen); } } // Folders - else if (openedWindow.folderUri) { - const pathToOpen = this.parseUri({ folderUri: openedWindow.folderUri }, { remoteAuthority: openedWindow.remoteAuthority }); - if (pathToOpen?.folderUri) { - windowsToOpen.push(pathToOpen); + else if (lastSessionWindow.folderUri) { + const pathToOpen = this.resolveOpenable({ folderUri: lastSessionWindow.folderUri }, { remoteAuthority: lastSessionWindow.remoteAuthority }); + if (isSingleFolderWorkspacePathToOpen(pathToOpen)) { + pathsToOpen.push(pathToOpen); } } // Empty window, potentially editors open to be restored - else if (restoreWindowsSetting !== 'folders' && openedWindow.backupPath) { - windowsToOpen.push({ backupPath: openedWindow.backupPath, remoteAuthority: openedWindow.remoteAuthority }); + else if (restoreWindowsSetting !== 'folders' && lastSessionWindow.backupPath) { + pathsToOpen.push({ backupPath: lastSessionWindow.backupPath, remoteAuthority: lastSessionWindow.remoteAuthority }); } } - if (windowsToOpen.length > 0) { - return windowsToOpen; - } - - break; + return pathsToOpen; } - - // Always fallback to empty window - return [Object.create(null)]; } private getRestoreWindowsSetting(): RestoreWindowsSetting { @@ -1064,75 +814,48 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return restoreWindows; } - private argToUri(arg: string): URI | undefined { - try { - const uri = URI.parse(arg); - if (!uri.scheme) { - this.logService.error(`Invalid URI input string, scheme missing: ${arg}`); - return undefined; - } + private resolveOpenable(openable: IWindowOpenable, options: IPathResolveOptions = {}): IPathToOpen | undefined { - return uri; - } catch (e) { - this.logService.error(`Invalid URI input string: ${arg}, ${e.message}`); + // handle file:// openables with some extra validation + let uri = this.resourceFromOpenable(openable); + if (uri.scheme === Schemas.file) { + return this.doResolveFileOpenable(openable, options); } - return undefined; + // handle non file:// openables + return this.doResolveRemoteOpenable(openable, options); } - private parseUri(toOpen: IWindowOpenable, options: IPathParseOptions = {}): IPathToOpen | undefined { - if (!toOpen) { - return undefined; - } - - let uri = this.resourceFromURIToOpen(toOpen); - if (uri.scheme === Schemas.file) { - return this.parsePath(uri.fsPath, options, isFileToOpen(toOpen)); - } + private doResolveRemoteOpenable(openable: IWindowOpenable, options: IPathResolveOptions): IPathToOpen | undefined { + let uri = this.resourceFromOpenable(openable); // open remote if either specified in the cli or if it's a remotehost URI const remoteAuthority = options.remoteAuthority || getRemoteAuthority(uri); // normalize URI - uri = normalizePath(uri); - - // remove trailing slash - uri = removeTrailingPathSeparator(uri); + uri = removeTrailingPathSeparator(normalizePath(uri)); // File - if (isFileToOpen(toOpen)) { + if (isFileToOpen(openable)) { if (options.gotoLineMode) { - const parsedPath = parseLineAndColumnAware(uri.path); - return { - fileUri: uri.with({ path: parsedPath.path }), - lineNumber: parsedPath.line, - columnNumber: parsedPath.column, - remoteAuthority - }; + const { path, line, column } = parseLineAndColumnAware(uri.path); + + return { fileUri: uri.with({ path }), lineNumber: line, columnNumber: column, remoteAuthority }; } - return { - fileUri: uri, - remoteAuthority - }; + return { fileUri: uri, remoteAuthority }; } // Workspace - else if (isWorkspaceToOpen(toOpen)) { - return { - workspace: getWorkspaceIdentifier(uri), - remoteAuthority - }; + else if (isWorkspaceToOpen(openable)) { + return { workspace: getWorkspaceIdentifier(uri), remoteAuthority }; } // Folder - return { - folderUri: uri, - remoteAuthority - }; + return { workspace: getSingleFolderWorkspaceIdentifier(uri), remoteAuthority }; } - private resourceFromURIToOpen(openable: IWindowOpenable): URI { + private resourceFromOpenable(openable: IWindowOpenable): URI { if (isWorkspaceToOpen(openable)) { return openable.workspaceUri; } @@ -1144,101 +867,113 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return openable.fileUri; } - private parsePath(anyPath: string, options: IPathParseOptions, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined { - if (!anyPath) { - return undefined; + private doResolveFileOpenable(path: string, options: IPathResolveOptions): IPathToOpen | undefined; + private doResolveFileOpenable(openable: IWindowOpenable, options: IPathResolveOptions): IPathToOpen | undefined; + private doResolveFileOpenable(pathOrOpenable: string | IWindowOpenable, options: IPathResolveOptions): IPathToOpen | undefined { + let path: string; + let forceOpenWorkspaceAsFile = false; + if (typeof pathOrOpenable === 'string') { + path = pathOrOpenable; + } else { + path = this.resourceFromOpenable(pathOrOpenable).fsPath; + forceOpenWorkspaceAsFile = isFileToOpen(pathOrOpenable); } + // Extract line/col information from path let lineNumber: number | undefined; let columnNumber: number | undefined; if (options.gotoLineMode) { - const parsedPath = parseLineAndColumnAware(anyPath); + const parsedPath = parseLineAndColumnAware(path); lineNumber = parsedPath.line; columnNumber = parsedPath.column; - anyPath = parsedPath.path; + path = parsedPath.path; } - // open remote if either specified in the cli even if it is a local file. + // With remote: resolve path as remote URI const remoteAuthority = options.remoteAuthority; if (remoteAuthority) { - const first = anyPath.charCodeAt(0); - - // make absolute - if (first !== CharCode.Slash) { - if (isWindowsDriveLetter(first) && anyPath.charCodeAt(anyPath.charCodeAt(1)) === CharCode.Colon) { - anyPath = toSlashes(anyPath); - } - - anyPath = `/${anyPath}`; - } - - const uri = URI.from({ scheme: Schemas.vscodeRemote, authority: remoteAuthority, path: anyPath }); - - // guess the file type: If it ends with a slash it's a folder. If it has a file extension, it's a file or a workspace. By defaults it's a folder. - if (anyPath.charCodeAt(anyPath.length - 1) !== CharCode.Slash) { - if (hasWorkspaceFileExtension(anyPath)) { - if (forceOpenWorkspaceAsFile) { - return { fileUri: uri, remoteAuthority }; - } - } else if (posix.basename(anyPath).indexOf('.') !== -1) { // file name starts with a dot or has an file extension - return { fileUri: uri, remoteAuthority }; - } - } - - return { folderUri: uri, remoteAuthority }; + return this.doResolvePathRemote(path, remoteAuthority, forceOpenWorkspaceAsFile); } - let candidate = normalize(anyPath); + // Without remote: resolve path as local URI + return this.doResolvePathLocal(path, options, lineNumber, columnNumber, forceOpenWorkspaceAsFile); + } + + private doResolvePathRemote(path: string, remoteAuthority: string, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined { + const first = path.charCodeAt(0); + + // make absolute + if (first !== CharCode.Slash) { + if (isWindowsDriveLetter(first) && path.charCodeAt(path.charCodeAt(1)) === CharCode.Colon) { + path = toSlashes(path); + } + + path = `/${path}`; + } + + const uri = URI.from({ scheme: Schemas.vscodeRemote, authority: remoteAuthority, path: path }); + + // guess the file type: + // - if it ends with a slash it's a folder + // - if it has a file extension, it's a file or a workspace + // - by defaults it's a folder + if (path.charCodeAt(path.length - 1) !== CharCode.Slash) { + + // file name ends with .code-workspace + if (hasWorkspaceFileExtension(path)) { + if (forceOpenWorkspaceAsFile) { + return { fileUri: uri, remoteAuthority }; + } + return { workspace: getWorkspaceIdentifier(uri), remoteAuthority }; + } + + // file name starts with a dot or has an file extension + else if (posix.basename(path).indexOf('.') !== -1) { + return { fileUri: uri, remoteAuthority }; + } + } + + return { workspace: getSingleFolderWorkspaceIdentifier(uri), remoteAuthority }; + } + + private doResolvePathLocal(path: string, options: IPathResolveOptions, lineNumber: number | undefined, columnNumber: number | undefined, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined { + + // Ensure the path is normalized and absolute + path = sanitizeFilePath(normalize(path), process.env['VSCODE_CWD'] || process.cwd()); try { - const candidateStat = statSync(candidate); - if (candidateStat.isFile()) { + const pathStat = statSync(path); + if (pathStat.isFile()) { // Workspace (unless disabled via flag) if (!forceOpenWorkspaceAsFile) { - const workspace = this.workspacesMainService.resolveLocalWorkspaceSync(URI.file(candidate)); + const workspace = this.workspacesManagementMainService.resolveLocalWorkspaceSync(URI.file(path)); if (workspace) { - return { - workspace: { id: workspace.id, configPath: workspace.configPath }, - remoteAuthority: workspace.remoteAuthority, - exists: true - }; + return { workspace: { id: workspace.id, configPath: workspace.configPath }, remoteAuthority: workspace.remoteAuthority, exists: true }; } } // File - return { - fileUri: URI.file(candidate), - lineNumber, - columnNumber, - remoteAuthority, - exists: true - }; + return { fileUri: URI.file(path), lineNumber, columnNumber, exists: true }; } // Folder (we check for isDirectory() because e.g. paths like /dev/null // are neither file nor folder but some external tools might pass them // over to us) - else if (candidateStat.isDirectory()) { - return { - folderUri: URI.file(candidate), - remoteAuthority, - exists: true - }; + else if (pathStat.isDirectory()) { + return { workspace: getSingleFolderWorkspaceIdentifier(URI.file(path), pathStat), exists: true }; } } catch (error) { - const fileUri = URI.file(candidate); - this.workspacesHistoryMainService.removeRecentlyOpened([fileUri]); // since file does not seem to exist anymore, remove from recent + const fileUri = URI.file(path); + + // since file does not seem to exist anymore, remove from recent + this.workspacesHistoryMainService.removeRecentlyOpened([fileUri]); // assume this is a file that does not yet exist if (options?.ignoreFileNotFound) { - return { - fileUri, - remoteAuthority, - exists: false - }; + return { fileUri, exists: false }; } } @@ -1287,12 +1022,12 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return { openFolderInNewWindow: !!openFolderInNewWindow, openFilesInNewWindow }; } - openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): ICodeWindow[] { + openExtensionDevelopmentHostWindow(extensionDevelopmentPaths: string[], openConfig: IOpenConfiguration): ICodeWindow[] { // Reload an existing extension development host window on the same path // We currently do not allow more than one extension development window // on the same extension path. - const existingWindow = findWindowOnExtensionDevelopmentPath(WindowsMainService.WINDOWS, extensionDevelopmentPath); + const existingWindow = findWindowOnExtensionDevelopmentPath(this.getWindows(), extensionDevelopmentPaths); if (existingWindow) { this.lifecycleMainService.reload(existingWindow, openConfig.cli); existingWindow.focus(); // make sure it gets focus and is restored @@ -1306,10 +1041,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Fill in previously opened workspace unless an explicit path is provided and we are not unit testing if (!cliArgs.length && !folderUris.length && !fileUris.length && !openConfig.cli.extensionTestsPath) { - const extensionDevelopmentWindowState = this.windowsState.lastPluginDevelopmentHostWindow; + const extensionDevelopmentWindowState = this.windowsStateHandler.state.lastPluginDevelopmentHostWindow; const workspaceToOpen = extensionDevelopmentWindowState && (extensionDevelopmentWindowState.workspace || extensionDevelopmentWindowState.folderUri); if (workspaceToOpen) { - if (isSingleFolderWorkspaceIdentifier(workspaceToOpen)) { + if (URI.isUri(workspaceToOpen)) { if (workspaceToOpen.scheme === Schemas.file) { cliArgs = [workspaceToOpen.fsPath]; } else { @@ -1326,9 +1061,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } let authority = ''; - for (let p of extensionDevelopmentPath) { - if (p.match(/^[a-zA-Z][a-zA-Z0-9\+\-\.]+:/)) { - const url = URI.parse(p); + for (const extensionDevelopmentPath of extensionDevelopmentPaths) { + if (extensionDevelopmentPath.match(/^[a-zA-Z][a-zA-Z0-9\+\-\.]+:/)) { + const url = URI.parse(extensionDevelopmentPath); if (url.scheme === Schemas.vscodeRemote) { if (authority) { if (url.authority !== authority) { @@ -1347,7 +1082,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic cliArgs = cliArgs.filter(path => { const uri = URI.file(path); - if (!!findWindowOnWorkspaceOrFolderUri(WindowsMainService.WINDOWS, uri)) { + if (!!findWindowOnWorkspaceOrFolder(this.getWindows(), uri)) { return false; } @@ -1355,8 +1090,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic }); folderUris = folderUris.filter(folderUriStr => { - const folderUri = this.argToUri(folderUriStr); - if (!!findWindowOnWorkspaceOrFolderUri(WindowsMainService.WINDOWS, folderUri)) { + const folderUri = this.cliArgToUri(folderUriStr); + if (folderUri && !!findWindowOnWorkspaceOrFolder(this.getWindows(), folderUri)) { return false; } @@ -1364,8 +1099,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic }); fileUris = fileUris.filter(fileUriStr => { - const fileUri = this.argToUri(fileUriStr); - if (!!findWindowOnWorkspaceOrFolderUri(WindowsMainService.WINDOWS, fileUri)) { + const fileUri = this.cliArgToUri(fileUriStr); + if (fileUri && !!findWindowOnWorkspaceOrFolder(this.getWindows(), fileUri)) { return false; } @@ -1398,7 +1133,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic private openInBrowserWindow(options: IOpenBrowserWindowOptions): ICodeWindow { - // Build INativeWindowConfiguration from config and options + // Build `INativeWindowConfiguration` from config and options const configuration = { ...options.cli } as INativeWindowConfiguration; configuration.appRoot = this.environmentService.appRoot; configuration.machineId = this.machineId; @@ -1408,7 +1143,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic configuration.userEnv = { ...this.initialUserEnv, ...options.userEnv }; configuration.isInitialStartup = options.initialStartup; configuration.workspace = options.workspace; - configuration.folderUri = options.folderUri; configuration.remoteAuthority = options.remoteAuthority; const filesToOpen = options.filesToOpen; @@ -1436,32 +1170,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // New window if (!window) { - const windowConfig = this.configurationService.getValue('window'); - const state = this.getNewWindowState(configuration); - - // Window state is not from a previous session: only allow fullscreen if we inherit it or user wants fullscreen - let allowFullscreen: boolean; - if (state.hasDefaultState) { - allowFullscreen = !!(windowConfig?.newWindowDimensions && ['fullscreen', 'inherit', 'offset'].indexOf(windowConfig.newWindowDimensions) >= 0); - } - - // Window state is from a previous session: only allow fullscreen when we got updated or user wants to restore - else { - allowFullscreen = !!(this.lifecycleMainService.wasRestarted || windowConfig?.restoreFullscreen); - - if (allowFullscreen && isMacintosh && WindowsMainService.WINDOWS.some(win => win.isFullScreen)) { - // macOS: Electron does not allow to restore multiple windows in - // fullscreen. As such, if we already restored a window in that - // state, we cannot allow more fullscreen windows. See - // https://github.com/microsoft/vscode/issues/41691 and - // https://github.com/electron/electron/issues/13077 - allowFullscreen = false; - } - } - - if (state.mode === WindowMode.Fullscreen && !allowFullscreen) { - state.mode = WindowMode.Normal; - } + const state = this.windowsStateHandler.getNewWindowState(configuration); // Create the window const createdWindow = window = this.instantiationService.createInstance(CodeWindow, { @@ -1485,12 +1194,12 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic this._onWindowOpened.fire(createdWindow); // Indicate number change via event - this._onWindowsCountChanged.fire({ oldCount: WindowsMainService.WINDOWS.length - 1, newCount: WindowsMainService.WINDOWS.length }); + this._onWindowsCountChanged.fire({ oldCount: this.getWindowCount() - 1, newCount: this.getWindowCount() }); // Window Events once(createdWindow.onReady)(() => this._onWindowReady.fire(createdWindow)); once(createdWindow.onClose)(() => this.onWindowClosed(createdWindow)); - once(createdWindow.onDestroy)(() => this.onBeforeWindowClose(createdWindow)); // try to save state before destroy because close will not fire + once(createdWindow.onDestroy)(() => this._onWindowDestroyed.fire(createdWindow)); createdWindow.win.webContents.removeAllListeners('devtools-reload-page'); // remove built in listener so we can handle this on our own createdWindow.win.webContents.on('devtools-reload-page', () => this.lifecycleMainService.reload(createdWindow)); @@ -1534,10 +1243,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Register window for backups if (!configuration.extensionDevelopmentPath) { - if (configuration.workspace) { + if (isWorkspaceIdentifier(configuration.workspace)) { configuration.backupPath = this.backupMainService.registerWorkspaceBackupSync({ workspace: configuration.workspace, remoteAuthority: configuration.remoteAuthority }); - } else if (configuration.folderUri) { - configuration.backupPath = this.backupMainService.registerFolderBackupSync(configuration.folderUri); + } else if (isSingleFolderWorkspaceIdentifier(configuration.workspace)) { + configuration.backupPath = this.backupMainService.registerFolderBackupSync(configuration.workspace.uri); } else { const backupFolder = options.emptyWindowBackupInfo && options.emptyWindowBackupInfo.backupFolder; configuration.backupPath = this.backupMainService.registerEmptyWindowBackupSync(backupFolder, configuration.remoteAuthority); @@ -1548,162 +1257,37 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic window.load(configuration); } - private getNewWindowState(configuration: INativeWindowConfiguration): INewWindowState { - const lastActive = this.getLastActiveWindow(); - - // Restore state unless we are running extension tests - if (!configuration.extensionTestsPath) { - - // extension development host Window - load from stored settings if any - if (!!configuration.extensionDevelopmentPath && this.windowsState.lastPluginDevelopmentHostWindow) { - return this.windowsState.lastPluginDevelopmentHostWindow.uiState; - } - - // Known Workspace - load from stored settings - const workspace = configuration.workspace; - if (workspace) { - const stateForWorkspace = this.windowsState.openedWindows.filter(o => o.workspace && o.workspace.id === workspace.id).map(o => o.uiState); - if (stateForWorkspace.length) { - return stateForWorkspace[0]; - } - } - - // Known Folder - load from stored settings - if (configuration.folderUri) { - const stateForFolder = this.windowsState.openedWindows.filter(o => o.folderUri && extUriBiasedIgnorePathCase.isEqual(o.folderUri, configuration.folderUri)).map(o => o.uiState); - if (stateForFolder.length) { - return stateForFolder[0]; - } - } - - // Empty windows with backups - else if (configuration.backupPath) { - const stateForEmptyWindow = this.windowsState.openedWindows.filter(o => o.backupPath === configuration.backupPath).map(o => o.uiState); - if (stateForEmptyWindow.length) { - return stateForEmptyWindow[0]; - } - } - - // First Window - const lastActiveState = this.lastClosedWindowState || this.windowsState.lastActiveWindow; - if (!lastActive && lastActiveState) { - return lastActiveState.uiState; - } - } - - // - // In any other case, we do not have any stored settings for the window state, so we come up with something smart - // - - // We want the new window to open on the same display that the last active one is in - let displayToUse: Display | undefined; - const displays = screen.getAllDisplays(); - - // Single Display - if (displays.length === 1) { - displayToUse = displays[0]; - } - - // Multi Display - else { - - // on mac there is 1 menu per window so we need to use the monitor where the cursor currently is - if (isMacintosh) { - const cursorPoint = screen.getCursorScreenPoint(); - displayToUse = screen.getDisplayNearestPoint(cursorPoint); - } - - // if we have a last active window, use that display for the new window - if (!displayToUse && lastActive) { - displayToUse = screen.getDisplayMatching(lastActive.getBounds()); - } - - // fallback to primary display or first display - if (!displayToUse) { - displayToUse = screen.getPrimaryDisplay() || displays[0]; - } - } - - // Compute x/y based on display bounds - // Note: important to use Math.round() because Electron does not seem to be too happy about - // display coordinates that are not absolute numbers. - let state = defaultWindowState(); - state.x = Math.round(displayToUse.bounds.x + (displayToUse.bounds.width / 2) - (state.width! / 2)); - state.y = Math.round(displayToUse.bounds.y + (displayToUse.bounds.height / 2) - (state.height! / 2)); - - // Check for newWindowDimensions setting and adjust accordingly - const windowConfig = this.configurationService.getValue('window'); - let ensureNoOverlap = true; - if (windowConfig?.newWindowDimensions) { - if (windowConfig.newWindowDimensions === 'maximized') { - state.mode = WindowMode.Maximized; - ensureNoOverlap = false; - } else if (windowConfig.newWindowDimensions === 'fullscreen') { - state.mode = WindowMode.Fullscreen; - ensureNoOverlap = false; - } else if ((windowConfig.newWindowDimensions === 'inherit' || windowConfig.newWindowDimensions === 'offset') && lastActive) { - const lastActiveState = lastActive.serializeWindowState(); - if (lastActiveState.mode === WindowMode.Fullscreen) { - state.mode = WindowMode.Fullscreen; // only take mode (fixes https://github.com/microsoft/vscode/issues/19331) - } else { - state = lastActiveState; - } - - ensureNoOverlap = state.mode !== WindowMode.Fullscreen && windowConfig.newWindowDimensions === 'offset'; - } - } - - if (ensureNoOverlap) { - state = this.ensureNoOverlap(state); - } - - (state as INewWindowState).hasDefaultState = true; // flag as default state - - return state; - } - - private ensureNoOverlap(state: ISingleWindowState): ISingleWindowState { - if (WindowsMainService.WINDOWS.length === 0) { - return state; - } - - state.x = typeof state.x === 'number' ? state.x : 0; - state.y = typeof state.y === 'number' ? state.y : 0; - - const existingWindowBounds = WindowsMainService.WINDOWS.map(win => win.getBounds()); - while (existingWindowBounds.some(b => b.x === state.x || b.y === state.y)) { - state.x += 30; - state.y += 30; - } - - return state; - } - - private onWindowClosed(win: ICodeWindow): void { + private onWindowClosed(window: ICodeWindow): void { // Remove from our list so that Electron can clean it up - const index = WindowsMainService.WINDOWS.indexOf(win); + const index = WindowsMainService.WINDOWS.indexOf(window); WindowsMainService.WINDOWS.splice(index, 1); // Emit - this._onWindowsCountChanged.fire({ oldCount: WindowsMainService.WINDOWS.length + 1, newCount: WindowsMainService.WINDOWS.length }); + this._onWindowsCountChanged.fire({ oldCount: this.getWindowCount() + 1, newCount: this.getWindowCount() }); } getFocusedWindow(): ICodeWindow | undefined { - const win = BrowserWindow.getFocusedWindow(); - if (win) { - return this.getWindowById(win.id); + const window = BrowserWindow.getFocusedWindow(); + if (window) { + return this.getWindowById(window.id); } return undefined; } getLastActiveWindow(): ICodeWindow | undefined { - return getLastActiveWindow(WindowsMainService.WINDOWS); + return this.doGetLastActiveWindow(this.getWindows()); } private getLastActiveWindowForAuthority(remoteAuthority: string | undefined): ICodeWindow | undefined { - return getLastActiveWindow(WindowsMainService.WINDOWS.filter(window => window.remoteAuthority === remoteAuthority)); + return this.doGetLastActiveWindow(this.getWindows().filter(window => window.remoteAuthority === remoteAuthority)); + } + + private doGetLastActiveWindow(windows: ICodeWindow[]): ICodeWindow | undefined { + const lastFocusedDate = Math.max.apply(Math, windows.map(window => window.lastFocusTime)); + + return windows.find(window => window.lastFocusTime === lastFocusedDate); } sendToFocused(channel: string, ...args: any[]): void { @@ -1715,7 +1299,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void { - for (const window of WindowsMainService.WINDOWS) { + for (const window of this.getWindows()) { if (windowIdsToIgnore && windowIdsToIgnore.indexOf(window.id) >= 0) { continue; // do not send if we are instructed to ignore it } @@ -1724,10 +1308,18 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } } - getWindowById(windowId: number): ICodeWindow | undefined { - const res = WindowsMainService.WINDOWS.filter(window => window.id === windowId); + getWindows(): ICodeWindow[] { + return WindowsMainService.WINDOWS; + } - return firstOrDefault(res); + getWindowCount(): number { + return WindowsMainService.WINDOWS.length; + } + + getWindowById(windowId: number): ICodeWindow | undefined { + const windows = this.getWindows().filter(window => window.id === windowId); + + return firstOrDefault(windows); } getWindowByWebContents(webContents: WebContents): ICodeWindow | undefined { @@ -1738,12 +1330,4 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return this.getWindowById(browserWindow.id); } - - getWindows(): ICodeWindow[] { - return WindowsMainService.WINDOWS; - } - - getWindowCount(): number { - return WindowsMainService.WINDOWS.length; - } } diff --git a/src/vs/platform/windows/electron-main/windowsStateHandler.ts b/src/vs/platform/windows/electron-main/windowsStateHandler.ts new file mode 100644 index 00000000..9b9558fa --- /dev/null +++ b/src/vs/platform/windows/electron-main/windowsStateHandler.ts @@ -0,0 +1,450 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { app, Display, screen } from 'electron'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { isMacintosh } from 'vs/base/common/platform'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { defaultWindowState } from 'vs/code/electron-main/window'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IStateService } from 'vs/platform/state/node/state'; +import { INativeWindowConfiguration, IWindowSettings } from 'vs/platform/windows/common/windows'; +import { ICodeWindow, IWindowsMainService, IWindowState as IWindowUIState, WindowMode } from 'vs/platform/windows/electron-main/windows'; +import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; + +export interface IWindowState { + workspace?: IWorkspaceIdentifier; + folderUri?: URI; + backupPath?: string; + remoteAuthority?: string; + uiState: IWindowUIState; +} + +export interface IWindowsState { + lastActiveWindow?: IWindowState; + lastPluginDevelopmentHostWindow?: IWindowState; + openedWindows: IWindowState[]; +} + +interface INewWindowState extends IWindowUIState { + hasDefaultState?: boolean; +} + +interface ISerializedWindowsState { + readonly lastActiveWindow?: ISerializedWindowState; + readonly lastPluginDevelopmentHostWindow?: ISerializedWindowState; + readonly openedWindows: ISerializedWindowState[]; +} + +interface ISerializedWindowState { + readonly workspaceIdentifier?: { id: string; configURIPath: string }; + readonly folder?: string; + readonly backupPath?: string; + readonly remoteAuthority?: string; + readonly uiState: IWindowUIState; +} + +export class WindowsStateHandler extends Disposable { + + private static readonly windowsStateStorageKey = 'windowsState'; + + get state() { return this._state; } + private readonly _state = restoreWindowsState(this.stateService.getItem(WindowsStateHandler.windowsStateStorageKey)); + + private lastClosedState: IWindowState | undefined = undefined; + + private shuttingDown = false; + + constructor( + @IWindowsMainService private readonly windowsMainService: IWindowsMainService, + @IStateService private readonly stateService: IStateService, + @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, + @ILogService private readonly logService: ILogService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + super(); + + this.registerListeners(); + } + + private registerListeners(): void { + + // When a window looses focus, save all windows state. This allows to + // prevent loss of window-state data when OS is restarted without properly + // shutting down the application (https://github.com/microsoft/vscode/issues/87171) + app.on('browser-window-blur', () => { + if (!this.shuttingDown) { + this.saveWindowsState(); + } + }); + + // Handle various lifecycle events around windows + this.lifecycleMainService.onBeforeWindowClose(window => this.onBeforeWindowClose(window)); + this.lifecycleMainService.onBeforeShutdown(() => this.onBeforeShutdown()); + this.windowsMainService.onWindowsCountChanged(e => { + if (e.newCount - e.oldCount > 0) { + // clear last closed window state when a new window opens. this helps on macOS where + // otherwise closing the last window, opening a new window and then quitting would + // use the state of the previously closed window when restarting. + this.lastClosedState = undefined; + } + }); + + // try to save state before destroy because close will not fire + this.windowsMainService.onWindowDestroyed(window => this.onBeforeWindowClose(window)); + } + + // Note that onBeforeShutdown() and onBeforeWindowClose() are fired in different order depending on the OS: + // - macOS: since the app will not quit when closing the last window, you will always first get + // the onBeforeShutdown() event followed by N onBeforeWindowClose() events for each window + // - other: on other OS, closing the last window will quit the app so the order depends on the + // user interaction: closing the last window will first trigger onBeforeWindowClose() + // and then onBeforeShutdown(). Using the quit action however will first issue onBeforeShutdown() + // and then onBeforeWindowClose(). + // + // Here is the behavior on different OS depending on action taken (Electron 1.7.x): + // + // Legend + // - quit(N): quit application with N windows opened + // - close(1): close one window via the window close button + // - closeAll: close all windows via the taskbar command + // - onBeforeShutdown(N): number of windows reported in this event handler + // - onBeforeWindowClose(N, M): number of windows reported and quitRequested boolean in this event handler + // + // macOS + // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) + // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) + // - quit(0): onBeforeShutdown(0) + // - close(1): onBeforeWindowClose(1, false) + // + // Windows + // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) + // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) + // - close(1): onBeforeWindowClose(2, false)[not last window] + // - close(1): onBeforeWindowClose(1, false), onBeforeShutdown(0)[last window] + // - closeAll(2): onBeforeWindowClose(2, false), onBeforeWindowClose(2, false), onBeforeShutdown(0) + // + // Linux + // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) + // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) + // - close(1): onBeforeWindowClose(2, false)[not last window] + // - close(1): onBeforeWindowClose(1, false), onBeforeShutdown(0)[last window] + // - closeAll(2): onBeforeWindowClose(2, false), onBeforeWindowClose(2, false), onBeforeShutdown(0) + // + private onBeforeShutdown(): void { + this.shuttingDown = true; + + this.saveWindowsState(); + } + + private saveWindowsState(): void { + const currentWindowsState: IWindowsState = { + openedWindows: [], + lastPluginDevelopmentHostWindow: this._state.lastPluginDevelopmentHostWindow, + lastActiveWindow: this.lastClosedState + }; + + // 1.) Find a last active window (pick any other first window otherwise) + if (!currentWindowsState.lastActiveWindow) { + let activeWindow = this.windowsMainService.getLastActiveWindow(); + if (!activeWindow || activeWindow.isExtensionDevelopmentHost) { + activeWindow = this.windowsMainService.getWindows().find(window => !window.isExtensionDevelopmentHost); + } + + if (activeWindow) { + currentWindowsState.lastActiveWindow = this.toWindowState(activeWindow); + } + } + + // 2.) Find extension host window + const extensionHostWindow = this.windowsMainService.getWindows().find(window => window.isExtensionDevelopmentHost && !window.isExtensionTestHost); + if (extensionHostWindow) { + currentWindowsState.lastPluginDevelopmentHostWindow = this.toWindowState(extensionHostWindow); + } + + // 3.) All windows (except extension host) for N >= 2 to support `restoreWindows: all` or for auto update + // + // Careful here: asking a window for its window state after it has been closed returns bogus values (width: 0, height: 0) + // so if we ever want to persist the UI state of the last closed window (window count === 1), it has + // to come from the stored lastClosedWindowState on Win/Linux at least + if (this.windowsMainService.getWindowCount() > 1) { + currentWindowsState.openedWindows = this.windowsMainService.getWindows().filter(window => !window.isExtensionDevelopmentHost).map(window => this.toWindowState(window)); + } + + // Persist + const state = getWindowsStateStoreData(currentWindowsState); + this.stateService.setItem(WindowsStateHandler.windowsStateStorageKey, state); + + if (this.shuttingDown) { + this.logService.trace('[WindowsStateHandler] onBeforeShutdown', state); + } + } + + // See note on #onBeforeShutdown() for details how these events are flowing + private onBeforeWindowClose(window: ICodeWindow): void { + if (this.lifecycleMainService.quitRequested) { + return; // during quit, many windows close in parallel so let it be handled in the before-quit handler + } + + // On Window close, update our stored UI state of this window + const state: IWindowState = this.toWindowState(window); + if (window.isExtensionDevelopmentHost && !window.isExtensionTestHost) { + this._state.lastPluginDevelopmentHostWindow = state; // do not let test run window state overwrite our extension development state + } + + // Any non extension host window with same workspace or folder + else if (!window.isExtensionDevelopmentHost && window.openedWorkspace) { + this._state.openedWindows.forEach(openedWindow => { + const sameWorkspace = isWorkspaceIdentifier(window.openedWorkspace) && openedWindow.workspace?.id === window.openedWorkspace.id; + const sameFolder = isSingleFolderWorkspaceIdentifier(window.openedWorkspace) && openedWindow.folderUri && extUriBiasedIgnorePathCase.isEqual(openedWindow.folderUri, window.openedWorkspace.uri); + + if (sameWorkspace || sameFolder) { + openedWindow.uiState = state.uiState; + } + }); + } + + // On Windows and Linux closing the last window will trigger quit. Since we are storing all UI state + // before quitting, we need to remember the UI state of this window to be able to persist it. + // On macOS we keep the last closed window state ready in case the user wants to quit right after or + // wants to open another window, in which case we use this state over the persisted one. + if (this.windowsMainService.getWindowCount() === 1) { + this.lastClosedState = state; + } + } + + private toWindowState(window: ICodeWindow): IWindowState { + return { + workspace: isWorkspaceIdentifier(window.openedWorkspace) ? window.openedWorkspace : undefined, + folderUri: isSingleFolderWorkspaceIdentifier(window.openedWorkspace) ? window.openedWorkspace.uri : undefined, + backupPath: window.backupPath, + remoteAuthority: window.remoteAuthority, + uiState: window.serializeWindowState() + }; + } + + getNewWindowState(configuration: INativeWindowConfiguration): INewWindowState { + const state = this.doGetNewWindowState(configuration); + const windowConfig = this.configurationService.getValue('window'); + + // Window state is not from a previous session: only allow fullscreen if we inherit it or user wants fullscreen + let allowFullscreen: boolean; + if (state.hasDefaultState) { + allowFullscreen = !!(windowConfig?.newWindowDimensions && ['fullscreen', 'inherit', 'offset'].indexOf(windowConfig.newWindowDimensions) >= 0); + } + + // Window state is from a previous session: only allow fullscreen when we got updated or user wants to restore + else { + allowFullscreen = !!(this.lifecycleMainService.wasRestarted || windowConfig?.restoreFullscreen); + + if (allowFullscreen && isMacintosh && this.windowsMainService.getWindows().some(window => window.isFullScreen)) { + // macOS: Electron does not allow to restore multiple windows in + // fullscreen. As such, if we already restored a window in that + // state, we cannot allow more fullscreen windows. See + // https://github.com/microsoft/vscode/issues/41691 and + // https://github.com/electron/electron/issues/13077 + allowFullscreen = false; + } + } + + if (state.mode === WindowMode.Fullscreen && !allowFullscreen) { + state.mode = WindowMode.Normal; + } + + return state; + } + + private doGetNewWindowState(configuration: INativeWindowConfiguration): INewWindowState { + const lastActive = this.windowsMainService.getLastActiveWindow(); + + // Restore state unless we are running extension tests + if (!configuration.extensionTestsPath) { + + // extension development host Window - load from stored settings if any + if (!!configuration.extensionDevelopmentPath && this.state.lastPluginDevelopmentHostWindow) { + return this.state.lastPluginDevelopmentHostWindow.uiState; + } + + // Known Workspace - load from stored settings + const workspace = configuration.workspace; + if (isWorkspaceIdentifier(workspace)) { + const stateForWorkspace = this.state.openedWindows.filter(openedWindow => openedWindow.workspace && openedWindow.workspace.id === workspace.id).map(openedWindow => openedWindow.uiState); + if (stateForWorkspace.length) { + return stateForWorkspace[0]; + } + } + + // Known Folder - load from stored settings + if (isSingleFolderWorkspaceIdentifier(workspace)) { + const stateForFolder = this.state.openedWindows.filter(openedWindow => openedWindow.folderUri && extUriBiasedIgnorePathCase.isEqual(openedWindow.folderUri, workspace.uri)).map(openedWindow => openedWindow.uiState); + if (stateForFolder.length) { + return stateForFolder[0]; + } + } + + // Empty windows with backups + else if (configuration.backupPath) { + const stateForEmptyWindow = this.state.openedWindows.filter(openedWindow => openedWindow.backupPath === configuration.backupPath).map(openedWindow => openedWindow.uiState); + if (stateForEmptyWindow.length) { + return stateForEmptyWindow[0]; + } + } + + // First Window + const lastActiveState = this.lastClosedState || this.state.lastActiveWindow; + if (!lastActive && lastActiveState) { + return lastActiveState.uiState; + } + } + + // + // In any other case, we do not have any stored settings for the window state, so we come up with something smart + // + + // We want the new window to open on the same display that the last active one is in + let displayToUse: Display | undefined; + const displays = screen.getAllDisplays(); + + // Single Display + if (displays.length === 1) { + displayToUse = displays[0]; + } + + // Multi Display + else { + + // on mac there is 1 menu per window so we need to use the monitor where the cursor currently is + if (isMacintosh) { + const cursorPoint = screen.getCursorScreenPoint(); + displayToUse = screen.getDisplayNearestPoint(cursorPoint); + } + + // if we have a last active window, use that display for the new window + if (!displayToUse && lastActive) { + displayToUse = screen.getDisplayMatching(lastActive.getBounds()); + } + + // fallback to primary display or first display + if (!displayToUse) { + displayToUse = screen.getPrimaryDisplay() || displays[0]; + } + } + + // Compute x/y based on display bounds + // Note: important to use Math.round() because Electron does not seem to be too happy about + // display coordinates that are not absolute numbers. + let state = defaultWindowState(); + state.x = Math.round(displayToUse.bounds.x + (displayToUse.bounds.width / 2) - (state.width! / 2)); + state.y = Math.round(displayToUse.bounds.y + (displayToUse.bounds.height / 2) - (state.height! / 2)); + + // Check for newWindowDimensions setting and adjust accordingly + const windowConfig = this.configurationService.getValue('window'); + let ensureNoOverlap = true; + if (windowConfig?.newWindowDimensions) { + if (windowConfig.newWindowDimensions === 'maximized') { + state.mode = WindowMode.Maximized; + ensureNoOverlap = false; + } else if (windowConfig.newWindowDimensions === 'fullscreen') { + state.mode = WindowMode.Fullscreen; + ensureNoOverlap = false; + } else if ((windowConfig.newWindowDimensions === 'inherit' || windowConfig.newWindowDimensions === 'offset') && lastActive) { + const lastActiveState = lastActive.serializeWindowState(); + if (lastActiveState.mode === WindowMode.Fullscreen) { + state.mode = WindowMode.Fullscreen; // only take mode (fixes https://github.com/microsoft/vscode/issues/19331) + } else { + state = lastActiveState; + } + + ensureNoOverlap = state.mode !== WindowMode.Fullscreen && windowConfig.newWindowDimensions === 'offset'; + } + } + + if (ensureNoOverlap) { + state = this.ensureNoOverlap(state); + } + + (state as INewWindowState).hasDefaultState = true; // flag as default state + + return state; + } + + private ensureNoOverlap(state: IWindowUIState): IWindowUIState { + if (this.windowsMainService.getWindows().length === 0) { + return state; + } + + state.x = typeof state.x === 'number' ? state.x : 0; + state.y = typeof state.y === 'number' ? state.y : 0; + + const existingWindowBounds = this.windowsMainService.getWindows().map(window => window.getBounds()); + while (existingWindowBounds.some(bounds => bounds.x === state.x || bounds.y === state.y)) { + state.x += 30; + state.y += 30; + } + + return state; + } +} + +export function restoreWindowsState(data: ISerializedWindowsState | undefined): IWindowsState { + const result: IWindowsState = { openedWindows: [] }; + const windowsState = data || { openedWindows: [] }; + + if (windowsState.lastActiveWindow) { + result.lastActiveWindow = restoreWindowState(windowsState.lastActiveWindow); + } + + if (windowsState.lastPluginDevelopmentHostWindow) { + result.lastPluginDevelopmentHostWindow = restoreWindowState(windowsState.lastPluginDevelopmentHostWindow); + } + + if (Array.isArray(windowsState.openedWindows)) { + result.openedWindows = windowsState.openedWindows.map(windowState => restoreWindowState(windowState)); + } + + return result; +} + +function restoreWindowState(windowState: ISerializedWindowState): IWindowState { + const result: IWindowState = { uiState: windowState.uiState }; + if (windowState.backupPath) { + result.backupPath = windowState.backupPath; + } + + if (windowState.remoteAuthority) { + result.remoteAuthority = windowState.remoteAuthority; + } + + if (windowState.folder) { + result.folderUri = URI.parse(windowState.folder); + } + + if (windowState.workspaceIdentifier) { + result.workspace = { id: windowState.workspaceIdentifier.id, configPath: URI.parse(windowState.workspaceIdentifier.configURIPath) }; + } + + return result; +} + +export function getWindowsStateStoreData(windowsState: IWindowsState): IWindowsState { + return { + lastActiveWindow: windowsState.lastActiveWindow && serializeWindowState(windowsState.lastActiveWindow), + lastPluginDevelopmentHostWindow: windowsState.lastPluginDevelopmentHostWindow && serializeWindowState(windowsState.lastPluginDevelopmentHostWindow), + openedWindows: windowsState.openedWindows.map(ws => serializeWindowState(ws)) + }; +} + +function serializeWindowState(windowState: IWindowState): ISerializedWindowState { + return { + workspaceIdentifier: windowState.workspace && { id: windowState.workspace.id, configURIPath: windowState.workspace.configPath.toString() }, + folder: windowState.folderUri && windowState.folderUri.toString(), + backupPath: windowState.backupPath, + remoteAuthority: windowState.remoteAuthority, + uiState: windowState.uiState + }; +} diff --git a/src/vs/platform/windows/electron-main/windowsStateStorage.ts b/src/vs/platform/windows/electron-main/windowsStateStorage.ts deleted file mode 100644 index 16533395..00000000 --- a/src/vs/platform/windows/electron-main/windowsStateStorage.ts +++ /dev/null @@ -1,93 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { URI, UriComponents } from 'vs/base/common/uri'; -import { IWindowState as IWindowUIState } from 'vs/platform/windows/electron-main/windows'; -import { IWindowState, IWindowsState } from 'vs/platform/windows/electron-main/windowsMainService'; - -export type WindowsStateStorageData = object; - -interface ISerializedWindowsState { - lastActiveWindow?: ISerializedWindowState; - lastPluginDevelopmentHostWindow?: ISerializedWindowState; - openedWindows: ISerializedWindowState[]; -} - -interface ISerializedWindowState { - workspaceIdentifier?: { id: string; configURIPath: string }; - folder?: string; - backupPath?: string; - remoteAuthority?: string; - uiState: IWindowUIState; - - // deprecated - folderUri?: UriComponents; - folderPath?: string; - workspace?: { id: string; configPath: string }; -} - -export function restoreWindowsState(data: WindowsStateStorageData | undefined): IWindowsState { - const result: IWindowsState = { openedWindows: [] }; - const windowsState = data as ISerializedWindowsState || { openedWindows: [] }; - - if (windowsState.lastActiveWindow) { - result.lastActiveWindow = restoreWindowState(windowsState.lastActiveWindow); - } - - if (windowsState.lastPluginDevelopmentHostWindow) { - result.lastPluginDevelopmentHostWindow = restoreWindowState(windowsState.lastPluginDevelopmentHostWindow); - } - - if (Array.isArray(windowsState.openedWindows)) { - result.openedWindows = windowsState.openedWindows.map(windowState => restoreWindowState(windowState)); - } - - return result; -} - -function restoreWindowState(windowState: ISerializedWindowState): IWindowState { - const result: IWindowState = { uiState: windowState.uiState }; - if (windowState.backupPath) { - result.backupPath = windowState.backupPath; - } - - if (windowState.remoteAuthority) { - result.remoteAuthority = windowState.remoteAuthority; - } - - if (windowState.folder) { - result.folderUri = URI.parse(windowState.folder); - } else if (windowState.folderUri) { - result.folderUri = URI.revive(windowState.folderUri); - } else if (windowState.folderPath) { - result.folderUri = URI.file(windowState.folderPath); - } - - if (windowState.workspaceIdentifier) { - result.workspace = { id: windowState.workspaceIdentifier.id, configPath: URI.parse(windowState.workspaceIdentifier.configURIPath) }; - } else if (windowState.workspace) { - result.workspace = { id: windowState.workspace.id, configPath: URI.file(windowState.workspace.configPath) }; - } - - return result; -} - -export function getWindowsStateStoreData(windowsState: IWindowsState): WindowsStateStorageData { - return { - lastActiveWindow: windowsState.lastActiveWindow && serializeWindowState(windowsState.lastActiveWindow), - lastPluginDevelopmentHostWindow: windowsState.lastPluginDevelopmentHostWindow && serializeWindowState(windowsState.lastPluginDevelopmentHostWindow), - openedWindows: windowsState.openedWindows.map(ws => serializeWindowState(ws)) - }; -} - -function serializeWindowState(windowState: IWindowState): ISerializedWindowState { - return { - workspaceIdentifier: windowState.workspace && { id: windowState.workspace.id, configURIPath: windowState.workspace.configPath.toString() }, - folder: windowState.folderUri && windowState.folderUri.toString(), - backupPath: windowState.backupPath, - remoteAuthority: windowState.remoteAuthority, - uiState: windowState.uiState - }; -} diff --git a/src/vs/platform/windows/node/window.ts b/src/vs/platform/windows/node/window.ts deleted file mode 100644 index 6859b36a..00000000 --- a/src/vs/platform/windows/node/window.ts +++ /dev/null @@ -1,150 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { URI } from 'vs/base/common/uri'; -import * as platform from 'vs/base/common/platform'; -import * as extpath from 'vs/base/common/extpath'; -import { IWorkspaceIdentifier, IResolvedWorkspace, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; -import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; - -export const enum OpenContext { - - // opening when running from the command line - CLI, - - // macOS only: opening from the dock (also when opening files to a running instance from desktop) - DOCK, - - // opening from the main application window - MENU, - - // opening from a file or folder dialog - DIALOG, - - // opening from the OS's UI - DESKTOP, - - // opening through the API - API -} - -export interface IWindowContext { - openedWorkspace?: IWorkspaceIdentifier; - openedFolderUri?: URI; - - extensionDevelopmentPath?: string[]; - lastFocusTime: number; -} - -export interface IBestWindowOrFolderOptions { - windows: W[]; - newWindow: boolean; - context: OpenContext; - fileUri?: URI; - codeSettingsFolder?: string; - localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | null; -} - -export function findBestWindowOrFolderForFile({ windows, newWindow, context, fileUri, localWorkspaceResolver: workspaceResolver }: IBestWindowOrFolderOptions): W | undefined { - if (!newWindow && fileUri && (context === OpenContext.DESKTOP || context === OpenContext.CLI || context === OpenContext.DOCK)) { - const windowOnFilePath = findWindowOnFilePath(windows, fileUri, workspaceResolver); - if (windowOnFilePath) { - return windowOnFilePath; - } - } - return !newWindow ? getLastActiveWindow(windows) : undefined; -} - -function findWindowOnFilePath(windows: W[], fileUri: URI, localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | null): W | null { - - // First check for windows with workspaces that have a parent folder of the provided path opened - for (const window of windows) { - const workspace = window.openedWorkspace; - if (workspace) { - const resolvedWorkspace = localWorkspaceResolver(workspace); - if (resolvedWorkspace) { - // workspace could be resolved: It's in the local file system - if (resolvedWorkspace.folders.some(folder => extUriBiasedIgnorePathCase.isEqualOrParent(fileUri, folder.uri))) { - return window; - } - } else { - // use the config path instead - if (extUriBiasedIgnorePathCase.isEqualOrParent(fileUri, workspace.configPath)) { - return window; - } - } - } - } - - // Then go with single folder windows that are parent of the provided file path - const singleFolderWindowsOnFilePath = windows.filter(window => window.openedFolderUri && extUriBiasedIgnorePathCase.isEqualOrParent(fileUri, window.openedFolderUri)); - if (singleFolderWindowsOnFilePath.length) { - return singleFolderWindowsOnFilePath.sort((a, b) => -(a.openedFolderUri!.path.length - b.openedFolderUri!.path.length))[0]; - } - - return null; -} - -export function getLastActiveWindow(windows: W[]): W | undefined { - const lastFocusedDate = Math.max.apply(Math, windows.map(window => window.lastFocusTime)); - - return windows.find(window => window.lastFocusTime === lastFocusedDate); -} - -export function findWindowOnWorkspace(windows: W[], workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)): W | null { - if (isSingleFolderWorkspaceIdentifier(workspace)) { - for (const window of windows) { - // match on folder - if (isSingleFolderWorkspaceIdentifier(workspace)) { - if (window.openedFolderUri && extUriBiasedIgnorePathCase.isEqual(window.openedFolderUri, workspace)) { - return window; - } - } - } - } else if (isWorkspaceIdentifier(workspace)) { - for (const window of windows) { - // match on workspace - if (window.openedWorkspace && window.openedWorkspace.id === workspace.id) { - return window; - } - } - } - return null; -} - -export function findWindowOnExtensionDevelopmentPath(windows: W[], extensionDevelopmentPaths: string[]): W | null { - - const matches = (uriString: string): boolean => { - return extensionDevelopmentPaths.some(p => extpath.isEqual(p, uriString, !platform.isLinux /* ignorecase */)); - }; - - for (const window of windows) { - // match on extension development path. The path can be one or more paths or uri strings, using paths.isEqual is not 100% correct but good enough - const currPaths = window.extensionDevelopmentPath; - if (currPaths?.some(p => matches(p))) { - return window; - } - } - - return null; -} - -export function findWindowOnWorkspaceOrFolderUri(windows: W[], uri: URI | undefined): W | null { - if (!uri) { - return null; - } - for (const window of windows) { - // check for workspace config path - if (window.openedWorkspace && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.configPath, uri)) { - return window; - } - - // check for folder path - if (window.openedFolderUri && extUriBiasedIgnorePathCase.isEqual(window.openedFolderUri, uri)) { - return window; - } - } - return null; -} diff --git a/src/vs/platform/windows/common/windowTracker.ts b/src/vs/platform/windows/node/windowTracker.ts similarity index 100% rename from src/vs/platform/windows/common/windowTracker.ts rename to src/vs/platform/windows/node/windowTracker.ts diff --git a/src/vs/platform/windows/test/electron-main/window.test.ts b/src/vs/platform/windows/test/electron-main/window.test.ts new file mode 100644 index 00000000..42f458f9 --- /dev/null +++ b/src/vs/platform/windows/test/electron-main/window.test.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as path from 'vs/base/common/path'; +import { findWindowOnFile } from 'vs/platform/windows/electron-main/windowsFinder'; +import { ICodeWindow, IWindowState } from 'vs/platform/windows/electron-main/windows'; +import { IWorkspaceIdentifier, toWorkspaceFolders } from 'vs/platform/workspaces/common/workspaces'; +import { URI } from 'vs/base/common/uri'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Event } from 'vs/base/common/event'; +import { UriDto } from 'vs/base/common/types'; +import { ICommandAction } from 'vs/platform/actions/common/actions'; +import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; +import { INativeWindowConfiguration } from 'vs/platform/windows/common/windows'; + +const fixturesFolder = getPathFromAmdModule(require, './fixtures'); + +const testWorkspace: IWorkspaceIdentifier = { + id: Date.now().toString(), + configPath: URI.file(path.join(fixturesFolder, 'workspaces.json')) +}; + +const testWorkspaceFolders = toWorkspaceFolders([{ path: path.join(fixturesFolder, 'vscode_workspace_1_folder') }, { path: path.join(fixturesFolder, 'vscode_workspace_2_folder') }], testWorkspace.configPath, extUriBiasedIgnorePathCase); +const localWorkspaceResolver = (workspace: any) => { return workspace === testWorkspace ? { id: testWorkspace.id, configPath: workspace.configPath, folders: testWorkspaceFolders } : null; }; + +function createTestCodeWindow(options: { lastFocusTime: number, openedFolderUri?: URI, openedWorkspace?: IWorkspaceIdentifier }): ICodeWindow { + return new class implements ICodeWindow { + onLoad: Event = Event.None; + onReady: Event = Event.None; + onClose: Event = Event.None; + onDestroy: Event = Event.None; + whenClosedOrLoaded: Promise = Promise.resolve(); + id: number = -1; + win: Electron.BrowserWindow = undefined!; + config: INativeWindowConfiguration | undefined; + openedWorkspace = options.openedFolderUri ? { id: '', uri: options.openedFolderUri } : options.openedWorkspace; + backupPath?: string | undefined; + remoteAuthority?: string | undefined; + isExtensionDevelopmentHost = false; + isExtensionTestHost = false; + lastFocusTime = options.lastFocusTime; + isFullScreen = false; + isReady = true; + hasHiddenTitleBarStyle = false; + + ready(): Promise { throw new Error('Method not implemented.'); } + setReady(): void { throw new Error('Method not implemented.'); } + addTabbedWindow(window: ICodeWindow): void { throw new Error('Method not implemented.'); } + load(config: INativeWindowConfiguration, options: { isReload?: boolean }): void { throw new Error('Method not implemented.'); } + reload(cli?: NativeParsedArgs): void { throw new Error('Method not implemented.'); } + focus(options?: { force: boolean; }): void { throw new Error('Method not implemented.'); } + close(): void { throw new Error('Method not implemented.'); } + getBounds(): Electron.Rectangle { throw new Error('Method not implemented.'); } + send(channel: string, ...args: any[]): void { throw new Error('Method not implemented.'); } + sendWhenReady(channel: string, token: CancellationToken, ...args: any[]): void { throw new Error('Method not implemented.'); } + toggleFullScreen(): void { throw new Error('Method not implemented.'); } + isMinimized(): boolean { throw new Error('Method not implemented.'); } + setRepresentedFilename(name: string): void { throw new Error('Method not implemented.'); } + getRepresentedFilename(): string | undefined { throw new Error('Method not implemented.'); } + setDocumentEdited(edited: boolean): void { throw new Error('Method not implemented.'); } + isDocumentEdited(): boolean { throw new Error('Method not implemented.'); } + handleTitleDoubleClick(): void { throw new Error('Method not implemented.'); } + updateTouchBar(items: UriDto[][]): void { throw new Error('Method not implemented.'); } + serializeWindowState(): IWindowState { throw new Error('Method not implemented'); } + dispose(): void { } + }; +} + +const vscodeFolderWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder')) }); +const lastActiveWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 3, openedFolderUri: undefined }); +const noVscodeFolderWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) }); +const windows: ICodeWindow[] = [ + vscodeFolderWindow, + lastActiveWindow, + noVscodeFolderWindow, +]; + +suite('WindowsFinder', () => { + + test('New window without folder when no windows exist', () => { + assert.strictEqual(findWindowOnFile([], URI.file('nonexisting'), localWorkspaceResolver), undefined); + assert.strictEqual(findWindowOnFile([], URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')), localWorkspaceResolver), undefined); + }); + + test('Existing window with folder', () => { + assert.strictEqual(findWindowOnFile(windows, URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')), localWorkspaceResolver), noVscodeFolderWindow); + + assert.strictEqual(findWindowOnFile(windows, URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), localWorkspaceResolver), vscodeFolderWindow); + + const window: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'nested_folder')) }); + assert.strictEqual(findWindowOnFile([window], URI.file(path.join(fixturesFolder, 'vscode_folder', 'nested_folder', 'subfolder', 'file.txt')), localWorkspaceResolver), window); + }); + + test('More specific existing window wins', () => { + const window: ICodeWindow = createTestCodeWindow({ lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) }); + const nestedFolderWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder')) }); + assert.strictEqual(findWindowOnFile([window, nestedFolderWindow], URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder', 'subfolder', 'file.txt')), localWorkspaceResolver), nestedFolderWindow); + }); + + test('Workspace folder wins', () => { + const window: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedWorkspace: testWorkspace }); + assert.strictEqual(findWindowOnFile([window], URI.file(path.join(fixturesFolder, 'vscode_workspace_2_folder', 'nested_vscode_folder', 'subfolder', 'file.txt')), localWorkspaceResolver), window); + }); +}); diff --git a/src/vs/platform/windows/test/electron-main/windowsStateStorage.test.ts b/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts similarity index 68% rename from src/vs/platform/windows/test/electron-main/windowsStateStorage.test.ts rename to src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts index 419caee0..956ab942 100644 --- a/src/vs/platform/windows/test/electron-main/windowsStateStorage.test.ts +++ b/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts @@ -6,12 +6,10 @@ import * as assert from 'assert'; import * as os from 'os'; import * as path from 'vs/base/common/path'; - -import { restoreWindowsState, getWindowsStateStoreData } from 'vs/platform/windows/electron-main/windowsStateStorage'; +import { restoreWindowsState, getWindowsStateStoreData, IWindowsState, IWindowState } from 'vs/platform/windows/electron-main/windowsStateHandler'; import { IWindowState as IWindowUIState, WindowMode } from 'vs/platform/windows/electron-main/windows'; import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { URI } from 'vs/base/common/uri'; -import { IWindowsState, IWindowState } from 'vs/platform/windows/electron-main/windowsMainService'; function getUIState(): IWindowUIState { return { @@ -30,15 +28,15 @@ function toWorkspace(uri: URI): IWorkspaceIdentifier { }; } function assertEqualURI(u1: URI | undefined, u2: URI | undefined, message?: string): void { - assert.equal(u1 && u1.toString(), u2 && u2.toString(), message); + assert.strictEqual(u1 && u1.toString(), u2 && u2.toString(), message); } function assertEqualWorkspace(w1: IWorkspaceIdentifier | undefined, w2: IWorkspaceIdentifier | undefined, message?: string): void { if (!w1 || !w2) { - assert.equal(w1, w2, message); + assert.strictEqual(w1, w2, message); return; } - assert.equal(w1.id, w2.id, message); + assert.strictEqual(w1.id, w2.id, message); assertEqualURI(w1.configPath, w2.configPath, message); } @@ -47,9 +45,9 @@ function assertEqualWindowState(expected: IWindowState | undefined, actual: IWin assert.deepEqual(expected, actual, message); return; } - assert.equal(expected.backupPath, actual.backupPath, message); + assert.strictEqual(expected.backupPath, actual.backupPath, message); assertEqualURI(expected.folderUri, actual.folderUri, message); - assert.equal(expected.remoteAuthority, actual.remoteAuthority, message); + assert.strictEqual(expected.remoteAuthority, actual.remoteAuthority, message); assertEqualWorkspace(expected.workspace, actual.workspace, message); assert.deepEqual(expected.uiState, actual.uiState, message); } @@ -57,7 +55,7 @@ function assertEqualWindowState(expected: IWindowState | undefined, actual: IWin function assertEqualWindowsState(expected: IWindowsState, actual: IWindowsState, message?: string) { assertEqualWindowState(expected.lastPluginDevelopmentHostWindow, actual.lastPluginDevelopmentHostWindow, message); assertEqualWindowState(expected.lastActiveWindow, actual.lastActiveWindow, message); - assert.equal(expected.openedWindows.length, actual.openedWindows.length, message); + assert.strictEqual(expected.openedWindows.length, actual.openedWindows.length, message); for (let i = 0; i < expected.openedWindows.length; i++) { assertEqualWindowState(expected.openedWindows[i], actual.openedWindows[i], message); } @@ -117,94 +115,6 @@ suite('Windows State Storing', () => { assertRestoring(windowState, 'lastPluginDevelopmentHostWindow'); }); - test('open 1_31', () => { - const v1_31_workspace = `{ - "openedWindows": [], - "lastActiveWindow": { - "workspace": { - "id": "a41787288b5e9cc1a61ba2dd84cd0d80", - "configPath": "/home/user/workspaces/code-and-docs.code-workspace" - }, - "backupPath": "/home/user/.config/Code - Insiders/Backups/a41787288b5e9cc1a61ba2dd84cd0d80", - "uiState": { - "mode": 0, - "x": 0, - "y": 27, - "width": 2560, - "height": 1364 - } - } - }`; - - let windowsState = restoreWindowsState(JSON.parse(v1_31_workspace)); - let expected: IWindowsState = { - openedWindows: [], - lastActiveWindow: { - backupPath: '/home/user/.config/Code - Insiders/Backups/a41787288b5e9cc1a61ba2dd84cd0d80', - uiState: { mode: WindowMode.Maximized, x: 0, y: 27, width: 2560, height: 1364 }, - workspace: { id: 'a41787288b5e9cc1a61ba2dd84cd0d80', configPath: URI.file('/home/user/workspaces/code-and-docs.code-workspace') } - } - }; - - assertEqualWindowsState(expected, windowsState, 'v1_31_workspace'); - - const v1_31_folder = `{ - "openedWindows": [], - "lastPluginDevelopmentHostWindow": { - "folderUri": { - "$mid": 1, - "fsPath": "/home/user/workspaces/testing/customdata", - "external": "file:///home/user/workspaces/testing/customdata", - "path": "/home/user/workspaces/testing/customdata", - "scheme": "file" - }, - "uiState": { - "mode": 1, - "x": 593, - "y": 617, - "width": 1625, - "height": 595 - } - } - }`; - - windowsState = restoreWindowsState(JSON.parse(v1_31_folder)); - expected = { - openedWindows: [], - lastPluginDevelopmentHostWindow: { - uiState: { mode: WindowMode.Normal, x: 593, y: 617, width: 1625, height: 595 }, - folderUri: URI.parse('file:///home/user/workspaces/testing/customdata') - } - }; - assertEqualWindowsState(expected, windowsState, 'v1_31_folder'); - - const v1_31_empty_window = ` { - "openedWindows": [ - ], - "lastActiveWindow": { - "backupPath": "C:\\\\Users\\\\Mike\\\\AppData\\\\Roaming\\\\Code\\\\Backups\\\\1549538599815", - "uiState": { - "mode": 0, - "x": -8, - "y": -8, - "width": 2576, - "height": 1344 - } - } - }`; - - windowsState = restoreWindowsState(JSON.parse(v1_31_empty_window)); - expected = { - openedWindows: [], - lastActiveWindow: { - backupPath: 'C:\\Users\\Mike\\AppData\\Roaming\\Code\\Backups\\1549538599815', - uiState: { mode: WindowMode.Maximized, x: -8, y: -8, width: 2576, height: 1344 } - } - }; - assertEqualWindowsState(expected, windowsState, 'v1_31_empty_window'); - - }); - test('open 1_32', () => { const v1_32_workspace = `{ "openedWindows": [], @@ -286,7 +196,5 @@ suite('Windows State Storing', () => { } }; assertEqualWindowsState(expected, windowsState, 'v1_32_empty_window'); - }); - }); diff --git a/src/vs/platform/windows/test/node/window.test.ts b/src/vs/platform/windows/test/node/window.test.ts deleted file mode 100644 index 576d30d1..00000000 --- a/src/vs/platform/windows/test/node/window.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import * as path from 'vs/base/common/path'; -import { IBestWindowOrFolderOptions, IWindowContext, findBestWindowOrFolderForFile, OpenContext } from 'vs/platform/windows/node/window'; -import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; -import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; -import { URI } from 'vs/base/common/uri'; -import { getPathFromAmdModule } from 'vs/base/common/amd'; - -const fixturesFolder = getPathFromAmdModule(require, './fixtures'); - -const testWorkspace: IWorkspaceIdentifier = { - id: Date.now().toString(), - configPath: URI.file(path.join(fixturesFolder, 'workspaces.json')) -}; - -const testWorkspaceFolders = toWorkspaceFolders([{ path: path.join(fixturesFolder, 'vscode_workspace_1_folder') }, { path: path.join(fixturesFolder, 'vscode_workspace_2_folder') }], testWorkspace.configPath); - -function options(custom?: Partial>): IBestWindowOrFolderOptions { - return { - windows: [], - newWindow: false, - context: OpenContext.CLI, - codeSettingsFolder: '_vscode', - localWorkspaceResolver: workspace => { return workspace === testWorkspace ? { id: testWorkspace.id, configPath: workspace.configPath, folders: testWorkspaceFolders } : null; }, - ...custom - }; -} - -const vscodeFolderWindow: IWindowContext = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder')) }; -const lastActiveWindow: IWindowContext = { lastFocusTime: 3, openedFolderUri: undefined }; -const noVscodeFolderWindow: IWindowContext = { lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) }; -const windows: IWindowContext[] = [ - vscodeFolderWindow, - lastActiveWindow, - noVscodeFolderWindow, -]; - -suite('WindowsFinder', () => { - - test('New window without folder when no windows exist', () => { - assert.equal(findBestWindowOrFolderForFile(options()), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')) - })), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), - newWindow: true - })), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), - })), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), - context: OpenContext.API - })), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')) - })), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'new_folder', 'new_file.txt')) - })), null); - }); - - test('New window without folder when windows exist', () => { - assert.equal(findBestWindowOrFolderForFile(options({ - windows, - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')), - newWindow: true - })), null); - }); - - test('Last active window', () => { - assert.equal(findBestWindowOrFolderForFile(options({ - windows - })), lastActiveWindow); - assert.equal(findBestWindowOrFolderForFile(options({ - windows, - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder2', 'file.txt')) - })), lastActiveWindow); - assert.equal(findBestWindowOrFolderForFile(options({ - windows: [lastActiveWindow, noVscodeFolderWindow], - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), - })), lastActiveWindow); - assert.equal(findBestWindowOrFolderForFile(options({ - windows, - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')), - context: OpenContext.API - })), lastActiveWindow); - }); - - test('Existing window with folder', () => { - assert.equal(findBestWindowOrFolderForFile(options({ - windows, - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')) - })), noVscodeFolderWindow); - assert.equal(findBestWindowOrFolderForFile(options({ - windows, - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')) - })), vscodeFolderWindow); - const window: IWindowContext = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'nested_folder')) }; - assert.equal(findBestWindowOrFolderForFile(options({ - windows: [window], - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'nested_folder', 'subfolder', 'file.txt')) - })), window); - }); - - test('More specific existing window wins', () => { - const window: IWindowContext = { lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) }; - const nestedFolderWindow: IWindowContext = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder')) }; - assert.equal(findBestWindowOrFolderForFile(options({ - windows: [window, nestedFolderWindow], - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder', 'subfolder', 'file.txt')) - })), nestedFolderWindow); - }); - - test('Workspace folder wins', () => { - const window: IWindowContext = { lastFocusTime: 1, openedWorkspace: testWorkspace }; - assert.equal(findBestWindowOrFolderForFile(options({ - windows: [window], - fileUri: URI.file(path.join(fixturesFolder, 'vscode_workspace_2_folder', 'nested_vscode_folder', 'subfolder', 'file.txt')) - })), window); - }); -}); diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index 72ca615f..38a11c92 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -4,16 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import * as resources from 'vs/base/common/resources'; +import { joinPath, basenameOrAuthority } from 'vs/base/common/resources'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { TernarySearchTree } from 'vs/base/common/map'; import { Event } from 'vs/base/common/event'; -import { IWorkspaceIdentifier, IStoredWorkspaceFolder, isRawFileWorkspaceFolder, isRawUriWorkspaceFolder, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, IStoredWorkspaceFolder, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IWorkspaceFolderProvider } from 'vs/base/common/labels'; export const IWorkspaceContextService = createDecorator('contextService'); export interface IWorkspaceContextService extends IWorkspaceFolderProvider { + readonly _serviceBrand: undefined; /** @@ -58,9 +59,9 @@ export interface IWorkspaceContextService extends IWorkspaceFolderProvider { getWorkspaceFolder(resource: URI): IWorkspaceFolder | null; /** - * Return `true` if the current workspace has the given identifier otherwise `false`. + * Return `true` if the current workspace has the given identifier or root URI otherwise `false`. */ - isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean; + isCurrentWorkspace(workspaceIdOrFolder: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI): boolean; /** * Returns if the provided resource is inside the workspace or not. @@ -80,14 +81,6 @@ export interface IWorkspaceFoldersChangeEvent { changed: IWorkspaceFolder[]; } -export namespace IWorkspace { - export function isIWorkspace(thing: unknown): thing is IWorkspace { - return !!(thing && typeof thing === 'object' - && typeof (thing as IWorkspace).id === 'string' - && Array.isArray((thing as IWorkspace).folders)); - } -} - export interface IWorkspace { /** @@ -106,6 +99,14 @@ export interface IWorkspace { readonly configuration?: URI | null; } +export function isWorkspace(thing: unknown): thing is IWorkspace { + const candidate = thing as IWorkspace | undefined; + + return !!(candidate && typeof candidate === 'object' + && typeof candidate.id === 'string' + && Array.isArray(candidate.folders)); +} + export interface IWorkspaceFolderData { /** @@ -125,15 +126,6 @@ export interface IWorkspaceFolderData { readonly index: number; } -export namespace IWorkspaceFolder { - export function isIWorkspaceFolder(thing: unknown): thing is IWorkspaceFolder { - return !!(thing && typeof thing === 'object' - && URI.isUri((thing as IWorkspaceFolder).uri) - && typeof (thing as IWorkspaceFolder).name === 'string' - && typeof (thing as IWorkspaceFolder).toResource === 'function'); - } -} - export interface IWorkspaceFolder extends IWorkspaceFolderData { /** @@ -142,6 +134,15 @@ export interface IWorkspaceFolder extends IWorkspaceFolderData { toResource: (relativePath: string) => URI; } +export function isWorkspaceFolder(thing: unknown): thing is IWorkspaceFolder { + const candidate = thing as IWorkspaceFolder; + + return !!(candidate && typeof candidate === 'object' + && URI.isUri(candidate.uri) + && typeof candidate.name === 'string' + && typeof candidate.toResource === 'function'); +} + export class Workspace implements IWorkspace { private _foldersMap: TernarySearchTree = TernarySearchTree.forUris(this._ignorePathCasing); @@ -222,7 +223,7 @@ export class WorkspaceFolder implements IWorkspaceFolder { } toResource(relativePath: string): URI { - return resources.joinPath(this.uri, relativePath); + return joinPath(this.uri, relativePath); } toJSON(): IWorkspaceFolderData { @@ -231,43 +232,5 @@ export class WorkspaceFolder implements IWorkspaceFolder { } export function toWorkspaceFolder(resource: URI): WorkspaceFolder { - return new WorkspaceFolder({ uri: resource, index: 0, name: resources.basenameOrAuthority(resource) }, { uri: resource.toString() }); -} - -export function toWorkspaceFolders(configuredFolders: IStoredWorkspaceFolder[], workspaceConfigFile: URI): WorkspaceFolder[] { - let result: WorkspaceFolder[] = []; - let seen: Set = new Set(); - - const relativeTo = resources.dirname(workspaceConfigFile); - for (let configuredFolder of configuredFolders) { - let uri: URI | null = null; - if (isRawFileWorkspaceFolder(configuredFolder)) { - if (configuredFolder.path) { - uri = resources.resolvePath(relativeTo, configuredFolder.path); - } - } else if (isRawUriWorkspaceFolder(configuredFolder)) { - try { - uri = URI.parse(configuredFolder.uri); - // this makes sure all workspace folder are absolute - if (uri.path[0] !== '/') { - uri = uri.with({ path: '/' + uri.path }); - } - } catch (e) { - console.warn(e); - // ignore - } - } - if (uri) { - // remove duplicates - let comparisonKey = resources.getComparisonKey(uri); - if (!seen.has(comparisonKey)) { - seen.add(comparisonKey); - - const name = configuredFolder.name || resources.basenameOrAuthority(uri); - result.push(new WorkspaceFolder({ uri, name, index: result.length }, configuredFolder)); - } - } - } - - return result; + return new WorkspaceFolder({ uri: resource, index: 0, name: basenameOrAuthority(resource) }, { uri: resource.toString() }); } diff --git a/src/vs/platform/workspace/test/common/workspace.test.ts b/src/vs/platform/workspace/test/common/workspace.test.ts index 1d3f78f7..9b26cb22 100644 --- a/src/vs/platform/workspace/test/common/workspace.test.ts +++ b/src/vs/platform/workspace/test/common/workspace.test.ts @@ -5,10 +5,11 @@ import * as assert from 'assert'; import * as path from 'vs/base/common/path'; -import { Workspace, toWorkspaceFolders, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { URI } from 'vs/base/common/uri'; -import { IRawFileWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; +import { IRawFileWorkspaceFolder, toWorkspaceFolders } from 'vs/platform/workspaces/common/workspaces'; import { isLinux, isWindows } from 'vs/base/common/platform'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; suite('Workspace', () => { @@ -70,7 +71,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with single absolute folder', () => { - const actual = toWorkspaceFolders([{ path: '/src/test' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 1); assert.equal(actual[0].uri.fsPath, testFolderUri.fsPath); @@ -80,7 +81,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with single relative folder', () => { - const actual = toWorkspaceFolders([{ path: './test' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: './test' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 1); assert.equal(actual[0].uri.fsPath, testFolderUri.fsPath); @@ -90,7 +91,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with single absolute folder with name', () => { - const actual = toWorkspaceFolders([{ path: '/src/test', name: 'hello' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test', name: 'hello' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 1); @@ -101,7 +102,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with multiple unique absolute folders', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3' }, { path: '/src/test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3' }, { path: '/src/test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 3); assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); @@ -121,7 +122,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with multiple unique absolute folders with names', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3', name: 'noName' }, { path: '/src/test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3', name: 'noName' }, { path: '/src/test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 3); assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); @@ -141,7 +142,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with multiple unique absolute and relative folders', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/abc/test3', name: 'noName' }, { path: './test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/abc/test3', name: 'noName' }, { path: './test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 3); assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); @@ -161,7 +162,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with multiple absolute folders with duplicates', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test2', name: 'noName' }, { path: '/src/test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test2', name: 'noName' }, { path: '/src/test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 2); assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); @@ -176,7 +177,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with multiple absolute and relative folders with duplicates', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3', name: 'noName' }, { path: './test3' }, { path: '/abc/test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3', name: 'noName' }, { path: './test3' }, { path: '/abc/test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 3); assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); @@ -196,7 +197,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with multiple absolute and relative folders with invalid paths', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '', name: 'noName' }, { path: './test3' }, { path: '/abc/test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '', name: 'noName' }, { path: './test3' }, { path: '/abc/test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 3); assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index d6643801..66413f2e 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -5,11 +5,11 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { localize } from 'vs/nls'; -import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceFolder, IWorkspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { URI, UriComponents } from 'vs/base/common/uri'; import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform'; import { extname, isAbsolute } from 'vs/base/common/path'; -import { dirname, resolvePath, isEqualAuthority, relativePath, extname as resourceExtname, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { extname as resourceExtname, extUriBiasedIgnorePathCase, IExtUri } from 'vs/base/common/resources'; import * as jsonEdit from 'vs/base/common/jsonEdit'; import * as json from 'vs/base/common/json'; import { Schemas } from 'vs/base/common/network'; @@ -22,9 +22,16 @@ import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; export const WORKSPACE_EXTENSION = 'code-workspace'; +const WORKSPACE_SUFFIX = `.${WORKSPACE_EXTENSION}`; export const WORKSPACE_FILTER = [{ name: localize('codeWorkspace', "Code Workspace"), extensions: [WORKSPACE_EXTENSION] }]; export const UNTITLED_WORKSPACE_NAME = 'workspace.json'; +export function hasWorkspaceFileExtension(path: string | URI) { + const ext = (typeof path === 'string') ? extname(path) : resourceExtname(path); + + return ext === WORKSPACE_SUFFIX; +} + export const IWorkspacesService = createDecorator('workspacesService'); export interface IWorkspacesService { @@ -48,6 +55,8 @@ export interface IWorkspacesService { getDirtyWorkspaces(): Promise>; } +//#region Workspaces Recently Opened + export interface IRecentlyOpened { workspaces: Array; files: IRecentFile[]; @@ -61,7 +70,7 @@ export interface IRecentWorkspace { } export interface IRecentFolder { - folderUri: ISingleFolderWorkspaceIdentifier; + folderUri: URI; label?: string; } @@ -82,36 +91,114 @@ export function isRecentFile(curr: IRecent): curr is IRecentFile { return curr.hasOwnProperty('fileUri'); } -/** - * A single folder workspace identifier is just the path to the folder. - */ -export type ISingleFolderWorkspaceIdentifier = URI; +//#endregion -export interface IWorkspaceIdentifier { +//#region Identifiers / Payload + +export interface IBaseWorkspaceIdentifier { + + /** + * Every workspace (multi-root, single folder or empty) + * has a unique identifier. It is not possible to open + * a workspace with the same `id` in multiple windows + */ id: string; +} + +/** + * A single folder workspace identifier is a path to a folder + id. + */ +export interface ISingleFolderWorkspaceIdentifier extends IBaseWorkspaceIdentifier { + + /** + * Folder path as `URI`. + */ + uri: URI; +} + +export function isSingleFolderWorkspaceIdentifier(obj: unknown): obj is ISingleFolderWorkspaceIdentifier { + const singleFolderIdentifier = obj as ISingleFolderWorkspaceIdentifier | undefined; + + return typeof singleFolderIdentifier?.id === 'string' && URI.isUri(singleFolderIdentifier.uri); +} + +/** + * A multi-root workspace identifier is a path to a workspace file + id. + */ +export interface IWorkspaceIdentifier extends IBaseWorkspaceIdentifier { + + /** + * Workspace config file path as `URI`. + */ configPath: URI; } -export function reviveWorkspaceIdentifier(workspace: { id: string, configPath: UriComponents; }): IWorkspaceIdentifier { - return { id: workspace.id, configPath: URI.revive(workspace.configPath) }; +export function toWorkspaceIdentifier(workspace: IWorkspace): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined { + + // Multi root + if (workspace.configuration) { + return { + id: workspace.id, + configPath: workspace.configuration + }; + } + + // Single folder + if (workspace.folders.length === 1) { + return { + id: workspace.id, + uri: workspace.folders[0].uri + }; + } + + // Empty workspace + return undefined; } -export function isStoredWorkspaceFolder(thing: unknown): thing is IStoredWorkspaceFolder { - return isRawFileWorkspaceFolder(thing) || isRawUriWorkspaceFolder(thing); +export function isWorkspaceIdentifier(obj: unknown): obj is IWorkspaceIdentifier { + const workspaceIdentifier = obj as IWorkspaceIdentifier | undefined; + + return typeof workspaceIdentifier?.id === 'string' && URI.isUri(workspaceIdentifier.configPath); } -export function isRawFileWorkspaceFolder(thing: any): thing is IRawFileWorkspaceFolder { - return thing - && typeof thing === 'object' - && typeof thing.path === 'string' - && (!thing.name || typeof thing.name === 'string'); +export function reviveIdentifier(identifier: { id: string, uri?: UriComponents, configPath?: UriComponents } | undefined): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined { + if (identifier?.uri) { + return { id: identifier.id, uri: URI.revive(identifier.uri) }; + } + + if (identifier?.configPath) { + return { id: identifier.id, configPath: URI.revive(identifier.configPath) }; + } + + return undefined; } -export function isRawUriWorkspaceFolder(thing: any): thing is IRawUriWorkspaceFolder { - return thing - && typeof thing === 'object' - && typeof thing.uri === 'string' - && (!thing.name || typeof thing.name === 'string'); +export function isUntitledWorkspace(path: URI, environmentService: IEnvironmentService): boolean { + return extUriBiasedIgnorePathCase.isEqualOrParent(path, environmentService.untitledWorkspacesHome); +} + +export interface IEmptyWorkspaceInitializationPayload extends IBaseWorkspaceIdentifier { } + +export type IWorkspaceInitializationPayload = IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceInitializationPayload; + +//#endregion + +//#region Workspace File Utilities + +export function isStoredWorkspaceFolder(obj: unknown): obj is IStoredWorkspaceFolder { + return isRawFileWorkspaceFolder(obj) || isRawUriWorkspaceFolder(obj); +} + +export function isRawFileWorkspaceFolder(obj: unknown): obj is IRawFileWorkspaceFolder { + const candidate = obj as IRawFileWorkspaceFolder | undefined; + + return typeof candidate?.path === 'string' && (!candidate.name || typeof candidate.name === 'string'); +} + +export function isRawUriWorkspaceFolder(obj: unknown): obj is IRawUriWorkspaceFolder { + const candidate = obj as IRawUriWorkspaceFolder | undefined; + + return typeof candidate?.uri === 'string' && (!candidate.name || typeof candidate.name === 'string'); } export interface IRawFileWorkspaceFolder { @@ -151,56 +238,6 @@ export interface IEnterWorkspaceResult { backupPath?: string; } -export function isSingleFolderWorkspaceIdentifier(obj: unknown): obj is ISingleFolderWorkspaceIdentifier { - return obj instanceof URI; -} - -export function isWorkspaceIdentifier(obj: unknown): obj is IWorkspaceIdentifier { - const workspaceIdentifier = obj as IWorkspaceIdentifier; - - return workspaceIdentifier && typeof workspaceIdentifier.id === 'string' && workspaceIdentifier.configPath instanceof URI; -} - -export function toWorkspaceIdentifier(workspace: IWorkspace): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined { - if (workspace.configuration) { - return { - configPath: workspace.configuration, - id: workspace.id - }; - } - - if (workspace.folders.length === 1) { - return workspace.folders[0].uri; - } - - // Empty workspace - return undefined; -} - -export function isUntitledWorkspace(path: URI, environmentService: IEnvironmentService): boolean { - return extUriBiasedIgnorePathCase.isEqualOrParent(path, environmentService.untitledWorkspacesHome); -} - -export type IMultiFolderWorkspaceInitializationPayload = IWorkspaceIdentifier; -export interface ISingleFolderWorkspaceInitializationPayload { id: string; folder: ISingleFolderWorkspaceIdentifier; } -export interface IEmptyWorkspaceInitializationPayload { id: string; } - -export type IWorkspaceInitializationPayload = IMultiFolderWorkspaceInitializationPayload | ISingleFolderWorkspaceInitializationPayload | IEmptyWorkspaceInitializationPayload; - -export function isSingleFolderWorkspaceInitializationPayload(obj: any): obj is ISingleFolderWorkspaceInitializationPayload { - return isSingleFolderWorkspaceIdentifier((obj.folder as ISingleFolderWorkspaceIdentifier)); -} - -const WORKSPACE_SUFFIX = '.' + WORKSPACE_EXTENSION; - -export function hasWorkspaceFileExtension(path: string | URI) { - const ext = (typeof path === 'string') ? extname(path) : resourceExtname(path); - - return ext === WORKSPACE_SUFFIX; -} - -const SLASH = '/'; - /** * Given a folder URI and the workspace config folder, computes the IStoredWorkspaceFolder using * a relative or absolute path or a uri. @@ -212,12 +249,12 @@ const SLASH = '/'; * @param targetConfigFolderURI the folder where the workspace is living in * @param useSlashForPath if set, use forward slashes for file paths on windows */ -export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, folderName: string | undefined, targetConfigFolderURI: URI, useSlashForPath = !isWindows): IStoredWorkspaceFolder { +export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, folderName: string | undefined, targetConfigFolderURI: URI, useSlashForPath = !isWindows, extUri: IExtUri): IStoredWorkspaceFolder { if (folderURI.scheme !== targetConfigFolderURI.scheme) { return { name: folderName, uri: folderURI.toString(true) }; } - let folderPath = !forceAbsolute ? relativePath(targetConfigFolderURI, folderURI) : undefined; + let folderPath = !forceAbsolute ? extUri.relativePath(targetConfigFolderURI, folderURI) : undefined; if (folderPath !== undefined) { if (folderPath.length === 0) { folderPath = '.'; @@ -241,7 +278,7 @@ export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, } } } else { - if (!isEqualAuthority(folderURI.authority, targetConfigFolderURI.authority)) { + if (!extUri.isEqualAuthority(folderURI.authority, targetConfigFolderURI.authority)) { return { name: folderName, uri: folderURI.toString(true) }; } folderPath = folderURI.path; @@ -251,21 +288,59 @@ export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, return { name: folderName, path: folderPath }; } +export function toWorkspaceFolders(configuredFolders: IStoredWorkspaceFolder[], workspaceConfigFile: URI, extUri: IExtUri): WorkspaceFolder[] { + let result: WorkspaceFolder[] = []; + let seen: Set = new Set(); + + const relativeTo = extUri.dirname(workspaceConfigFile); + for (let configuredFolder of configuredFolders) { + let uri: URI | null = null; + if (isRawFileWorkspaceFolder(configuredFolder)) { + if (configuredFolder.path) { + uri = extUri.resolvePath(relativeTo, configuredFolder.path); + } + } else if (isRawUriWorkspaceFolder(configuredFolder)) { + try { + uri = URI.parse(configuredFolder.uri); + // this makes sure all workspace folder are absolute + if (uri.path[0] !== '/') { + uri = uri.with({ path: '/' + uri.path }); + } + } catch (e) { + console.warn(e); + // ignore + } + } + if (uri) { + // remove duplicates + let comparisonKey = extUri.getComparisonKey(uri); + if (!seen.has(comparisonKey)) { + seen.add(comparisonKey); + + const name = configuredFolder.name || extUri.basenameOrAuthority(uri); + result.push(new WorkspaceFolder({ uri, name, index: result.length }, configuredFolder)); + } + } + } + + return result; +} + /** * Rewrites the content of a workspace file to be saved at a new location. * Throws an exception if file is not a valid workspace file */ -export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, configPathURI: URI, isFromUntitledWorkspace: boolean, targetConfigPathURI: URI) { +export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, configPathURI: URI, isFromUntitledWorkspace: boolean, targetConfigPathURI: URI, extUri: IExtUri) { let storedWorkspace = doParseStoredWorkspace(configPathURI, rawWorkspaceContents); - const sourceConfigFolder = dirname(configPathURI); - const targetConfigFolder = dirname(targetConfigPathURI); + const sourceConfigFolder = extUri.dirname(configPathURI); + const targetConfigFolder = extUri.dirname(targetConfigPathURI); const rewrittenFolders: IStoredWorkspaceFolder[] = []; const slashForPath = useSlashForPath(storedWorkspace.folders); for (const folder of storedWorkspace.folders) { - const folderURI = isRawFileWorkspaceFolder(folder) ? resolvePath(sourceConfigFolder, folder.path) : URI.parse(folder.uri); + const folderURI = isRawFileWorkspaceFolder(folder) ? extUri.resolvePath(sourceConfigFolder, folder.path) : URI.parse(folder.uri); let absolute; if (isFromUntitledWorkspace) { // if it was an untitled workspace, try to make paths relative @@ -274,7 +349,7 @@ export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, // for existing workspaces, preserve whether a path was absolute or relative absolute = !isRawFileWorkspaceFolder(folder) || isAbsolute(folder.path); } - rewrittenFolders.push(getStoredWorkspaceFolder(folderURI, absolute, folder.name, targetConfigFolder, slashForPath)); + rewrittenFolders.push(getStoredWorkspaceFolder(folderURI, absolute, folder.name, targetConfigFolder, slashForPath, extUri)); } // Preserve as much of the existing workspace as possible by using jsonEdit @@ -308,12 +383,14 @@ function doParseStoredWorkspace(path: URI, contents: string): IStoredWorkspace { export function useSlashForPath(storedFolders: IStoredWorkspaceFolder[]): boolean { if (isWindows) { - return storedFolders.some(folder => isRawFileWorkspaceFolder(folder) && folder.path.indexOf(SLASH) >= 0); + return storedFolders.some(folder => isRawFileWorkspaceFolder(folder) && folder.path.indexOf('/') >= 0); } return true; } +//#endregion + //#region Workspace Storage interface ISerializedRecentlyOpened { diff --git a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts index 7a5baa22..adef3f00 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts @@ -3,18 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import * as arrays from 'vs/base/common/arrays'; +import { localize } from 'vs/nls'; +import { coalesce } from 'vs/base/common/arrays'; import { IStateService } from 'vs/platform/state/node/state'; -import { app, JumpListCategory } from 'electron'; +import { app, JumpListCategory, JumpListItem } from 'electron'; import { ILogService } from 'vs/platform/log/common/log'; import { getBaseLabel, getPathLabel, splitName } from 'vs/base/common/labels'; import { Event as CommonEvent, Emitter } from 'vs/base/common/event'; import { isWindows, isMacintosh } from 'vs/base/common/platform'; -import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IRecentlyOpened, isRecentWorkspace, isRecentFolder, IRecent, isRecentFile, IRecentFolder, IRecentWorkspace, IRecentFile, toStoreData, restoreRecentlyOpened, RecentlyOpenedStorageData, WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces'; -import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; +import { IWorkspaceIdentifier, IRecentlyOpened, isRecentWorkspace, isRecentFolder, IRecent, isRecentFile, IRecentFolder, IRecentWorkspace, IRecentFile, toStoreData, restoreRecentlyOpened, RecentlyOpenedStorageData, WORKSPACE_EXTENSION, isWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { ThrottledDelayer } from 'vs/base/common/async'; -import { isEqual, dirname, originalFSPath, basename, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { dirname, originalFSPath, basename, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; @@ -57,15 +57,15 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa declare readonly _serviceBrand: undefined; - private readonly _onRecentlyOpenedChange = new Emitter(); + private readonly _onRecentlyOpenedChange = this._register(new Emitter()); readonly onRecentlyOpenedChange: CommonEvent = this._onRecentlyOpenedChange.event; - private macOSRecentDocumentsUpdater = this._register(new ThrottledDelayer(800)); + private readonly macOSRecentDocumentsUpdater = this._register(new ThrottledDelayer(800)); constructor( @IStateService private readonly stateService: IStateService, @ILogService private readonly logService: ILogService, - @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, + @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService ) { @@ -80,7 +80,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => this.handleWindowsJumpList()); // Add to history when entering workspace - this._register(this.workspacesMainService.onWorkspaceEntered(event => this.addRecentlyOpened([{ workspace: event.workspace }]))); + this._register(this.workspacesManagementMainService.onWorkspaceEntered(event => this.addRecentlyOpened([{ workspace: event.workspace }]))); } private handleWindowsJumpList(): void { @@ -89,40 +89,40 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa } this.updateWindowsJumpList(); - this.onRecentlyOpenedChange(() => this.updateWindowsJumpList()); + this._register(this.onRecentlyOpenedChange(() => this.updateWindowsJumpList())); } - addRecentlyOpened(newlyAdded: IRecent[]): void { + addRecentlyOpened(recentToAdd: IRecent[]): void { const workspaces: Array = []; const files: IRecentFile[] = []; - for (let curr of newlyAdded) { + for (let recent of recentToAdd) { // Workspace - if (isRecentWorkspace(curr)) { - if (!this.workspacesMainService.isUntitledWorkspace(curr.workspace) && indexOfWorkspace(workspaces, curr.workspace) === -1) { - workspaces.push(curr); + if (isRecentWorkspace(recent)) { + if (!this.workspacesManagementMainService.isUntitledWorkspace(recent.workspace) && indexOfWorkspace(workspaces, recent.workspace) === -1) { + workspaces.push(recent); } } // Folder - else if (isRecentFolder(curr)) { - if (indexOfFolder(workspaces, curr.folderUri) === -1) { - workspaces.push(curr); + else if (isRecentFolder(recent)) { + if (indexOfFolder(workspaces, recent.folderUri) === -1) { + workspaces.push(recent); } } // File else { - const alreadyExistsInHistory = indexOfFile(files, curr.fileUri) >= 0; - const shouldBeFiltered = curr.fileUri.scheme === Schemas.file && WorkspacesHistoryMainService.COMMON_FILES_FILTER.indexOf(basename(curr.fileUri)) >= 0; + const alreadyExistsInHistory = indexOfFile(files, recent.fileUri) >= 0; + const shouldBeFiltered = recent.fileUri.scheme === Schemas.file && WorkspacesHistoryMainService.COMMON_FILES_FILTER.indexOf(basename(recent.fileUri)) >= 0; if (!alreadyExistsInHistory && !shouldBeFiltered) { - files.push(curr); + files.push(recent); // Add to recent documents (Windows only, macOS later) - if (isWindows && curr.fileUri.scheme === Schemas.file) { - app.addRecentDocument(curr.fileUri.fsPath); + if (isWindows && recent.fileUri.scheme === Schemas.file) { + app.addRecentDocument(recent.fileUri.fsPath); } } } @@ -147,14 +147,15 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa } } - removeRecentlyOpened(toRemove: URI[]): void { + removeRecentlyOpened(recentToRemove: URI[]): void { const keep = (recent: IRecent) => { const uri = location(recent); - for (const resource of toRemove) { - if (isEqual(resource, uri)) { + for (const resourceToRemove of recentToRemove) { + if (extUriBiasedIgnorePathCase.isEqual(resourceToRemove, uri)) { return false; } } + return true; }; @@ -246,13 +247,10 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa // Add current workspace to beginning if set const currentWorkspace = include?.config?.workspace; - if (currentWorkspace && !this.workspacesMainService.isUntitledWorkspace(currentWorkspace)) { + if (isWorkspaceIdentifier(currentWorkspace) && !this.workspacesManagementMainService.isUntitledWorkspace(currentWorkspace)) { workspaces.push({ workspace: currentWorkspace }); - } - - const currentFolder = include?.config?.folderUri; - if (currentFolder) { - workspaces.push({ folderUri: currentFolder }); + } else if (isSingleFolderWorkspaceIdentifier(currentWorkspace)) { + workspaces.push({ folderUri: currentWorkspace.uri }); } // Add currently files to open to the beginning if any @@ -319,8 +317,8 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa items: [ { type: 'task', - title: nls.localize('newWindow', "New Window"), - description: nls.localize('newWindowDesc', "Opens a new window"), + title: localize('newWindow', "New Window"), + description: localize('newWindowDesc', "Opens a new window"), program: process.execPath, args: '-n', // force new window iconPath: process.execPath, @@ -330,57 +328,59 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa }); // Recent Workspaces - try { - if (this.getRecentlyOpened().workspaces.length > 0) { + if (this.getRecentlyOpened().workspaces.length > 0) { - // The user might have meanwhile removed items from the jump list and we have to respect that - // so we need to update our list of recent paths with the choice of the user to not add them again - // Also: Windows will not show our custom category at all if there is any entry which was removed - // by the user! See https://github.com/microsoft/vscode/issues/15052 - let toRemove: URI[] = []; - for (let item of app.getJumpListSettings().removedItems) { - const args = item.args; - if (args) { - const match = /^--(folder|file)-uri\s+"([^"]+)"$/.exec(args); - if (match) { - toRemove.push(URI.parse(match[2])); - } + // The user might have meanwhile removed items from the jump list and we have to respect that + // so we need to update our list of recent paths with the choice of the user to not add them again + // Also: Windows will not show our custom category at all if there is any entry which was removed + // by the user! See https://github.com/microsoft/vscode/issues/15052 + let toRemove: URI[] = []; + for (let item of app.getJumpListSettings().removedItems) { + const args = item.args; + if (args) { + const match = /^--(folder|file)-uri\s+"([^"]+)"$/.exec(args); + if (match) { + toRemove.push(URI.parse(match[2])); } } - this.removeRecentlyOpened(toRemove); + } + this.removeRecentlyOpened(toRemove); - // Add entries + // Add entries + let hasWorkspaces = false; + const items: JumpListItem[] = coalesce(this.getRecentlyOpened().workspaces.slice(0, 7 /* limit number of entries here */).map(recent => { + const workspace = isRecentWorkspace(recent) ? recent.workspace : recent.folderUri; + const title = recent.label ? splitName(recent.label).name : this.getSimpleWorkspaceLabel(workspace, this.environmentService.untitledWorkspacesHome); + + let description; + let args; + if (URI.isUri(workspace)) { + description = localize('folderDesc', "{0} {1}", getBaseLabel(workspace), getPathLabel(dirname(workspace), this.environmentService)); + args = `--folder-uri "${workspace.toString()}"`; + } else { + hasWorkspaces = true; + description = localize('workspaceDesc', "{0} {1}", getBaseLabel(workspace.configPath), getPathLabel(dirname(workspace.configPath), this.environmentService)); + args = `--file-uri "${workspace.configPath.toString()}"`; + } + + return { + type: 'task', + title: title.substr(0, 255), // Windows seems to be picky around the length of entries + description: description.substr(0, 255), // (see https://github.com/microsoft/vscode/issues/111177) + program: process.execPath, + args, + iconPath: 'explorer.exe', // simulate folder icon + iconIndex: 0 + }; + })); + + if (items.length > 0) { jumpList.push({ type: 'custom', - name: nls.localize('recentFolders', "Recent Workspaces"), - items: arrays.coalesce(this.getRecentlyOpened().workspaces.slice(0, 7 /* limit number of entries here */).map(recent => { - const workspace = isRecentWorkspace(recent) ? recent.workspace : recent.folderUri; - const title = recent.label ? splitName(recent.label).name : this.getSimpleWorkspaceLabel(workspace, this.environmentService.untitledWorkspacesHome); - - let description; - let args; - if (isSingleFolderWorkspaceIdentifier(workspace)) { - description = nls.localize('folderDesc', "{0} {1}", getBaseLabel(workspace), getPathLabel(dirname(workspace), this.environmentService)); - args = `--folder-uri "${workspace.toString()}"`; - } else { - description = nls.localize('workspaceDesc', "{0} {1}", getBaseLabel(workspace.configPath), getPathLabel(dirname(workspace.configPath), this.environmentService)); - args = `--file-uri "${workspace.configPath.toString()}"`; - } - - return { - type: 'task', - title, - description, - program: process.execPath, - args, - iconPath: 'explorer.exe', // simulate folder icon - iconIndex: 0 - }; - })) + name: hasWorkspaces ? localize('recentFoldersAndWorkspaces', "Recent Folders & Workspaces") : localize('recentFolders', "Recent Folders"), + items }); } - } catch (error) { - this.logService.warn('updateWindowsJumpList#recentWorkspaces', error); // https://github.com/microsoft/vscode/issues/111177 } // Recent @@ -396,21 +396,24 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa } private getSimpleWorkspaceLabel(workspace: IWorkspaceIdentifier | URI, workspaceHome: URI): string { - if (isSingleFolderWorkspaceIdentifier(workspace)) { + + // Single Folder + if (URI.isUri(workspace)) { return basename(workspace); } // Workspace: Untitled if (extUriBiasedIgnorePathCase.isEqualOrParent(workspace.configPath, workspaceHome)) { - return nls.localize('untitledWorkspace', "Untitled (Workspace)"); + return localize('untitledWorkspace', "Untitled (Workspace)"); } + // Workspace: normal let filename = basename(workspace.configPath); if (filename.endsWith(WORKSPACE_EXTENSION)) { filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1); } - return nls.localize('workspaceName', "{0} (Workspace)", filename); + return localize('workspaceName', "{0} (Workspace)", filename); } } @@ -430,10 +433,10 @@ function indexOfWorkspace(arr: IRecent[], candidate: IWorkspaceIdentifier): numb return arr.findIndex(workspace => isRecentWorkspace(workspace) && workspace.workspace.id === candidate.id); } -function indexOfFolder(arr: IRecent[], candidate: ISingleFolderWorkspaceIdentifier): number { - return arr.findIndex(folder => isRecentFolder(folder) && isEqual(folder.folderUri, candidate)); +function indexOfFolder(arr: IRecent[], candidate: URI): number { + return arr.findIndex(folder => isRecentFolder(folder) && extUriBiasedIgnorePathCase.isEqual(folder.folderUri, candidate)); } function indexOfFile(arr: IRecentFile[], candidate: URI): number { - return arr.findIndex(file => isEqual(file.fileUri, candidate)); + return arr.findIndex(file => extUriBiasedIgnorePathCase.isEqual(file.fileUri, candidate)); } diff --git a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts index f4fa065f..5328a07d 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -3,337 +3,79 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IWorkspaceIdentifier, hasWorkspaceFileExtension, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, IUntitledWorkspaceInfo, getStoredWorkspaceFolder, IEnterWorkspaceResult, isUntitledWorkspace } from 'vs/platform/workspaces/common/workspaces'; -import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; -import { join, dirname } from 'vs/base/common/path'; -import { mkdirp, writeFile, rimrafSync, readdirSync, writeFileSync } from 'vs/base/node/pfs'; -import { readFileSync, existsSync, mkdirSync } from 'fs'; -import { isLinux } from 'vs/base/common/platform'; -import { Event, Emitter } from 'vs/base/common/event'; -import { ILogService } from 'vs/platform/log/common/log'; -import { createHash } from 'crypto'; -import * as json from 'vs/base/common/json'; -import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; +import { AddFirstParameterToFunctions } from 'vs/base/common/types'; +import { IWorkspacesService, IEnterWorkspaceResult, IWorkspaceFolderCreationData, IWorkspaceIdentifier, IRecentlyOpened, IRecent } from 'vs/platform/workspaces/common/workspaces'; import { URI } from 'vs/base/common/uri'; -import { Schemas } from 'vs/base/common/network'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { originalFSPath, joinPath, basename, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; -import { localize } from 'vs/nls'; -import product from 'vs/platform/product/common/product'; -import { MessageBoxOptions, BrowserWindow } from 'electron'; -import { withNullAsUndefined } from 'vs/base/common/types'; +import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; +import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; +import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; -import { findWindowOnWorkspace } from 'vs/platform/windows/node/window'; -export const IWorkspacesMainService = createDecorator('workspacesMainService'); - -export interface IWorkspaceEnteredEvent { - window: ICodeWindow; - workspace: IWorkspaceIdentifier; -} - -export interface IWorkspacesMainService { - - readonly _serviceBrand: undefined; - - readonly onUntitledWorkspaceDeleted: Event; - readonly onWorkspaceEntered: Event; - - enterWorkspace(intoWindow: ICodeWindow, openedWindows: ICodeWindow[], path: URI): Promise; - - createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise; - createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier; - - deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise; - deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void; - - getUntitledWorkspacesSync(): IUntitledWorkspaceInfo[]; - isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean; - - resolveLocalWorkspaceSync(path: URI): IResolvedWorkspace | null; - getWorkspaceIdentifier(workspacePath: URI): Promise; -} - -export interface IStoredWorkspace { - folders: IStoredWorkspaceFolder[]; - remoteAuthority?: string; -} - -export class WorkspacesMainService extends Disposable implements IWorkspacesMainService { +export class WorkspacesMainService implements AddFirstParameterToFunctions /* only methods, not events */, number /* window ID */> { declare readonly _serviceBrand: undefined; - private readonly untitledWorkspacesHome: URI; // local URI that contains all untitled workspaces - - private readonly _onUntitledWorkspaceDeleted = this._register(new Emitter()); - readonly onUntitledWorkspaceDeleted: Event = this._onUntitledWorkspaceDeleted.event; - - private readonly _onWorkspaceEntered = this._register(new Emitter()); - readonly onWorkspaceEntered: Event = this._onWorkspaceEntered.event; - constructor( - @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, - @ILogService private readonly logService: ILogService, - @IBackupMainService private readonly backupMainService: IBackupMainService, - @IDialogMainService private readonly dialogMainService: IDialogMainService + @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, + @IWindowsMainService private readonly windowsMainService: IWindowsMainService, + @IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService, + @IBackupMainService private readonly backupMainService: IBackupMainService ) { - super(); - - this.untitledWorkspacesHome = environmentService.untitledWorkspacesHome; } - resolveLocalWorkspaceSync(uri: URI): IResolvedWorkspace | null { - if (!this.isWorkspacePath(uri)) { - return null; // does not look like a valid workspace config file - } - if (uri.scheme !== Schemas.file) { - return null; - } + //#region Workspace Management - let contents: string; - try { - contents = readFileSync(uri.fsPath, 'utf8'); - } catch (error) { - return null; // invalid workspace - } - - return this.doResolveWorkspace(uri, contents); - } - - private isWorkspacePath(uri: URI): boolean { - return isUntitledWorkspace(uri, this.environmentService) || hasWorkspaceFileExtension(uri); - } - - private doResolveWorkspace(path: URI, contents: string): IResolvedWorkspace | null { - try { - const workspace = this.doParseStoredWorkspace(path, contents); - const workspaceIdentifier = getWorkspaceIdentifier(path); - return { - id: workspaceIdentifier.id, - configPath: workspaceIdentifier.configPath, - folders: toWorkspaceFolders(workspace.folders, workspaceIdentifier.configPath), - remoteAuthority: workspace.remoteAuthority - }; - } catch (error) { - this.logService.warn(error.toString()); + async enterWorkspace(windowId: number, path: URI): Promise { + const window = this.windowsMainService.getWindowById(windowId); + if (window) { + return this.workspacesManagementMainService.enterWorkspace(window, this.windowsMainService.getWindows(), path); } return null; } - private doParseStoredWorkspace(path: URI, contents: string): IStoredWorkspace { - - // Parse workspace file - let storedWorkspace: IStoredWorkspace = json.parse(contents); // use fault tolerant parser - - // Filter out folders which do not have a path or uri set - if (storedWorkspace && Array.isArray(storedWorkspace.folders)) { - storedWorkspace.folders = storedWorkspace.folders.filter(folder => isStoredWorkspaceFolder(folder)); - } else { - throw new Error(`${path.toString(true)} looks like an invalid workspace file.`); - } - - return storedWorkspace; + createUntitledWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise { + return this.workspacesManagementMainService.createUntitledWorkspace(folders, remoteAuthority); } - async createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise { - const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority); - const configPath = workspace.configPath.fsPath; - - await mkdirp(dirname(configPath)); - await writeFile(configPath, JSON.stringify(storedWorkspace, null, '\t')); - - return workspace; + deleteUntitledWorkspace(windowId: number, workspace: IWorkspaceIdentifier): Promise { + return this.workspacesManagementMainService.deleteUntitledWorkspace(workspace); } - createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): IWorkspaceIdentifier { - const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority); - const configPath = workspace.configPath.fsPath; - - const configPathDir = dirname(configPath); - if (!existsSync(configPathDir)) { - const configPathDirDir = dirname(configPathDir); - if (!existsSync(configPathDirDir)) { - mkdirSync(configPathDirDir); - } - mkdirSync(configPathDir); - } - - writeFileSync(configPath, JSON.stringify(storedWorkspace, null, '\t')); - - return workspace; + getWorkspaceIdentifier(windowId: number, workspacePath: URI): Promise { + return this.workspacesManagementMainService.getWorkspaceIdentifier(workspacePath); } - private newUntitledWorkspace(folders: IWorkspaceFolderCreationData[] = [], remoteAuthority?: string): { workspace: IWorkspaceIdentifier, storedWorkspace: IStoredWorkspace } { - const randomId = (Date.now() + Math.round(Math.random() * 1000)).toString(); - const untitledWorkspaceConfigFolder = joinPath(this.untitledWorkspacesHome, randomId); - const untitledWorkspaceConfigPath = joinPath(untitledWorkspaceConfigFolder, UNTITLED_WORKSPACE_NAME); + //#endregion - const storedWorkspaceFolder: IStoredWorkspaceFolder[] = []; + //#region Workspaces History - for (const folder of folders) { - storedWorkspaceFolder.push(getStoredWorkspaceFolder(folder.uri, true, folder.name, untitledWorkspaceConfigFolder)); - } + readonly onRecentlyOpenedChange = this.workspacesHistoryMainService.onRecentlyOpenedChange; - return { - workspace: getWorkspaceIdentifier(untitledWorkspaceConfigPath), - storedWorkspace: { folders: storedWorkspaceFolder, remoteAuthority } - }; + async getRecentlyOpened(windowId: number): Promise { + return this.workspacesHistoryMainService.getRecentlyOpened(this.windowsMainService.getWindowById(windowId)); } - async getWorkspaceIdentifier(configPath: URI): Promise { - return getWorkspaceIdentifier(configPath); + async addRecentlyOpened(windowId: number, recents: IRecent[]): Promise { + return this.workspacesHistoryMainService.addRecentlyOpened(recents); } - isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean { - return isUntitledWorkspace(workspace.configPath, this.environmentService); + async removeRecentlyOpened(windowId: number, paths: URI[]): Promise { + return this.workspacesHistoryMainService.removeRecentlyOpened(paths); } - deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void { - if (!this.isUntitledWorkspace(workspace)) { - return; // only supported for untitled workspaces - } - - // Delete from disk - this.doDeleteUntitledWorkspaceSync(workspace); - - // Event - this._onUntitledWorkspaceDeleted.fire(workspace); + async clearRecentlyOpened(windowId: number): Promise { + return this.workspacesHistoryMainService.clearRecentlyOpened(); } - async deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise { - this.deleteUntitledWorkspaceSync(workspace); + //#endregion + + + //#region Dirty Workspaces + + async getDirtyWorkspaces(): Promise> { + return this.backupMainService.getDirtyWorkspaces(); } - private doDeleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void { - const configPath = originalFSPath(workspace.configPath); - try { - - // Delete Workspace - rimrafSync(dirname(configPath)); - - // Mark Workspace Storage to be deleted - const workspaceStoragePath = join(this.environmentService.workspaceStorageHome.fsPath, workspace.id); - if (existsSync(workspaceStoragePath)) { - writeFileSync(join(workspaceStoragePath, 'obsolete'), ''); - } - } catch (error) { - this.logService.warn(`Unable to delete untitled workspace ${configPath} (${error}).`); - } - } - - getUntitledWorkspacesSync(): IUntitledWorkspaceInfo[] { - let untitledWorkspaces: IUntitledWorkspaceInfo[] = []; - try { - const untitledWorkspacePaths = readdirSync(this.untitledWorkspacesHome.fsPath).map(folder => joinPath(this.untitledWorkspacesHome, folder, UNTITLED_WORKSPACE_NAME)); - for (const untitledWorkspacePath of untitledWorkspacePaths) { - const workspace = getWorkspaceIdentifier(untitledWorkspacePath); - const resolvedWorkspace = this.resolveLocalWorkspaceSync(untitledWorkspacePath); - if (!resolvedWorkspace) { - this.doDeleteUntitledWorkspaceSync(workspace); - } else { - untitledWorkspaces.push({ workspace, remoteAuthority: resolvedWorkspace.remoteAuthority }); - } - } - } catch (error) { - if (error.code !== 'ENOENT') { - this.logService.warn(`Unable to read folders in ${this.untitledWorkspacesHome} (${error}).`); - } - } - return untitledWorkspaces; - } - - async enterWorkspace(window: ICodeWindow, windows: ICodeWindow[], path: URI): Promise { - if (!window || !window.win || !window.isReady) { - return null; // return early if the window is not ready or disposed - } - - const isValid = await this.isValidTargetWorkspacePath(window, windows, path); - if (!isValid) { - return null; // return early if the workspace is not valid - } - - const result = this.doEnterWorkspace(window, getWorkspaceIdentifier(path)); - if (!result) { - return null; - } - - // Emit as event - this._onWorkspaceEntered.fire({ window, workspace: result.workspace }); - - return result; - } - - private async isValidTargetWorkspacePath(window: ICodeWindow, windows: ICodeWindow[], path?: URI): Promise { - if (!path) { - return true; - } - - if (window.openedWorkspace && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.configPath, path)) { - return false; // window is already opened on a workspace with that path - } - - // Prevent overwriting a workspace that is currently opened in another window - if (findWindowOnWorkspace(windows, getWorkspaceIdentifier(path))) { - const options: MessageBoxOptions = { - title: product.nameLong, - type: 'info', - buttons: [localize('ok', "OK")], - message: localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(path)), - detail: localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again."), - noLink: true - }; - - await this.dialogMainService.showMessageBox(options, withNullAsUndefined(BrowserWindow.getFocusedWindow())); - - return false; - } - - return true; // OK - } - - private doEnterWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): IEnterWorkspaceResult | null { - if (!window.config) { - return null; - } - - window.focus(); - - // Register window for backups and migrate current backups over - let backupPath: string | undefined; - if (!window.config.extensionDevelopmentPath) { - backupPath = this.backupMainService.registerWorkspaceBackupSync({ workspace, remoteAuthority: window.remoteAuthority }, window.config.backupPath); - } - - // if the window was opened on an untitled workspace, delete it. - if (window.openedWorkspace && this.isUntitledWorkspace(window.openedWorkspace)) { - this.deleteUntitledWorkspaceSync(window.openedWorkspace); - } - - // Update window configuration properly based on transition to workspace - window.config.folderUri = undefined; - window.config.workspace = workspace; - window.config.backupPath = backupPath; - - return { workspace, backupPath }; - } -} - -function getWorkspaceId(configPath: URI): string { - let workspaceConfigPath = configPath.scheme === Schemas.file ? originalFSPath(configPath) : configPath.toString(); - if (!isLinux) { - workspaceConfigPath = workspaceConfigPath.toLowerCase(); // sanitize for platform file system - } - - return createHash('md5').update(workspaceConfigPath).digest('hex'); -} - -export function getWorkspaceIdentifier(configPath: URI): IWorkspaceIdentifier { - return { - configPath, - id: getWorkspaceId(configPath) - }; + //#endregion } diff --git a/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts new file mode 100644 index 00000000..306d1daa --- /dev/null +++ b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts @@ -0,0 +1,394 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { toWorkspaceFolders, IWorkspaceIdentifier, hasWorkspaceFileExtension, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, IUntitledWorkspaceInfo, getStoredWorkspaceFolder, IEnterWorkspaceResult, isUntitledWorkspace, isWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; +import { join, dirname } from 'vs/base/common/path'; +import { mkdirp, writeFile, rimrafSync, readdirSync, writeFileSync } from 'vs/base/node/pfs'; +import { readFileSync, existsSync, mkdirSync, statSync, Stats } from 'fs'; +import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; +import { Event, Emitter } from 'vs/base/common/event'; +import { ILogService } from 'vs/platform/log/common/log'; +import { createHash } from 'crypto'; +import { parse } from 'vs/base/common/json'; +import { URI } from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { originalFSPath, joinPath, basename, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; +import { localize } from 'vs/nls'; +import product from 'vs/platform/product/common/product'; +import { MessageBoxOptions, BrowserWindow } from 'electron'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; +import { findWindowOnWorkspaceOrFolder } from 'vs/platform/windows/electron-main/windowsFinder'; + +export const IWorkspacesManagementMainService = createDecorator('workspacesManagementMainService'); + +export interface IWorkspaceEnteredEvent { + window: ICodeWindow; + workspace: IWorkspaceIdentifier; +} + +export interface IWorkspacesManagementMainService { + + readonly _serviceBrand: undefined; + + readonly onUntitledWorkspaceDeleted: Event; + readonly onWorkspaceEntered: Event; + + enterWorkspace(intoWindow: ICodeWindow, openedWindows: ICodeWindow[], path: URI): Promise; + + createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise; + createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier; + + deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise; + deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void; + + getUntitledWorkspacesSync(): IUntitledWorkspaceInfo[]; + isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean; + + resolveLocalWorkspaceSync(path: URI): IResolvedWorkspace | null; + getWorkspaceIdentifier(workspacePath: URI): Promise; +} + +export interface IStoredWorkspace { + folders: IStoredWorkspaceFolder[]; + remoteAuthority?: string; +} + +export class WorkspacesManagementMainService extends Disposable implements IWorkspacesManagementMainService { + + declare readonly _serviceBrand: undefined; + + private readonly untitledWorkspacesHome = this.environmentService.untitledWorkspacesHome; // local URI that contains all untitled workspaces + + private readonly _onUntitledWorkspaceDeleted = this._register(new Emitter()); + readonly onUntitledWorkspaceDeleted: Event = this._onUntitledWorkspaceDeleted.event; + + private readonly _onWorkspaceEntered = this._register(new Emitter()); + readonly onWorkspaceEntered: Event = this._onWorkspaceEntered.event; + + constructor( + @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, + @ILogService private readonly logService: ILogService, + @IBackupMainService private readonly backupMainService: IBackupMainService, + @IDialogMainService private readonly dialogMainService: IDialogMainService + ) { + super(); + } + + resolveLocalWorkspaceSync(uri: URI): IResolvedWorkspace | null { + if (!this.isWorkspacePath(uri)) { + return null; // does not look like a valid workspace config file + } + if (uri.scheme !== Schemas.file) { + return null; + } + + let contents: string; + try { + contents = readFileSync(uri.fsPath, 'utf8'); + } catch (error) { + return null; // invalid workspace + } + + return this.doResolveWorkspace(uri, contents); + } + + private isWorkspacePath(uri: URI): boolean { + return isUntitledWorkspace(uri, this.environmentService) || hasWorkspaceFileExtension(uri); + } + + private doResolveWorkspace(path: URI, contents: string): IResolvedWorkspace | null { + try { + const workspace = this.doParseStoredWorkspace(path, contents); + const workspaceIdentifier = getWorkspaceIdentifier(path); + return { + id: workspaceIdentifier.id, + configPath: workspaceIdentifier.configPath, + folders: toWorkspaceFolders(workspace.folders, workspaceIdentifier.configPath, extUriBiasedIgnorePathCase), + remoteAuthority: workspace.remoteAuthority + }; + } catch (error) { + this.logService.warn(error.toString()); + } + + return null; + } + + private doParseStoredWorkspace(path: URI, contents: string): IStoredWorkspace { + + // Parse workspace file + const storedWorkspace: IStoredWorkspace = parse(contents); // use fault tolerant parser + + // Filter out folders which do not have a path or uri set + if (storedWorkspace && Array.isArray(storedWorkspace.folders)) { + storedWorkspace.folders = storedWorkspace.folders.filter(folder => isStoredWorkspaceFolder(folder)); + } else { + throw new Error(`${path.toString(true)} looks like an invalid workspace file.`); + } + + return storedWorkspace; + } + + async createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise { + const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority); + const configPath = workspace.configPath.fsPath; + + await mkdirp(dirname(configPath)); + await writeFile(configPath, JSON.stringify(storedWorkspace, null, '\t')); + + return workspace; + } + + createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): IWorkspaceIdentifier { + const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority); + const configPath = workspace.configPath.fsPath; + + const configPathDir = dirname(configPath); + if (!existsSync(configPathDir)) { + const configPathDirDir = dirname(configPathDir); + if (!existsSync(configPathDirDir)) { + mkdirSync(configPathDirDir); + } + mkdirSync(configPathDir); + } + + writeFileSync(configPath, JSON.stringify(storedWorkspace, null, '\t')); + + return workspace; + } + + private newUntitledWorkspace(folders: IWorkspaceFolderCreationData[] = [], remoteAuthority?: string): { workspace: IWorkspaceIdentifier, storedWorkspace: IStoredWorkspace } { + const randomId = (Date.now() + Math.round(Math.random() * 1000)).toString(); + const untitledWorkspaceConfigFolder = joinPath(this.untitledWorkspacesHome, randomId); + const untitledWorkspaceConfigPath = joinPath(untitledWorkspaceConfigFolder, UNTITLED_WORKSPACE_NAME); + + const storedWorkspaceFolder: IStoredWorkspaceFolder[] = []; + + for (const folder of folders) { + storedWorkspaceFolder.push(getStoredWorkspaceFolder(folder.uri, true, folder.name, untitledWorkspaceConfigFolder, !isWindows, extUriBiasedIgnorePathCase)); + } + + return { + workspace: getWorkspaceIdentifier(untitledWorkspaceConfigPath), + storedWorkspace: { folders: storedWorkspaceFolder, remoteAuthority } + }; + } + + async getWorkspaceIdentifier(configPath: URI): Promise { + return getWorkspaceIdentifier(configPath); + } + + isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean { + return isUntitledWorkspace(workspace.configPath, this.environmentService); + } + + deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void { + if (!this.isUntitledWorkspace(workspace)) { + return; // only supported for untitled workspaces + } + + // Delete from disk + this.doDeleteUntitledWorkspaceSync(workspace); + + // Event + this._onUntitledWorkspaceDeleted.fire(workspace); + } + + async deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise { + this.deleteUntitledWorkspaceSync(workspace); + } + + private doDeleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void { + const configPath = originalFSPath(workspace.configPath); + try { + + // Delete Workspace + rimrafSync(dirname(configPath)); + + // Mark Workspace Storage to be deleted + const workspaceStoragePath = join(this.environmentService.workspaceStorageHome.fsPath, workspace.id); + if (existsSync(workspaceStoragePath)) { + writeFileSync(join(workspaceStoragePath, 'obsolete'), ''); + } + } catch (error) { + this.logService.warn(`Unable to delete untitled workspace ${configPath} (${error}).`); + } + } + + getUntitledWorkspacesSync(): IUntitledWorkspaceInfo[] { + const untitledWorkspaces: IUntitledWorkspaceInfo[] = []; + try { + const untitledWorkspacePaths = readdirSync(this.untitledWorkspacesHome.fsPath).map(folder => joinPath(this.untitledWorkspacesHome, folder, UNTITLED_WORKSPACE_NAME)); + for (const untitledWorkspacePath of untitledWorkspacePaths) { + const workspace = getWorkspaceIdentifier(untitledWorkspacePath); + const resolvedWorkspace = this.resolveLocalWorkspaceSync(untitledWorkspacePath); + if (!resolvedWorkspace) { + this.doDeleteUntitledWorkspaceSync(workspace); + } else { + untitledWorkspaces.push({ workspace, remoteAuthority: resolvedWorkspace.remoteAuthority }); + } + } + } catch (error) { + if (error.code !== 'ENOENT') { + this.logService.warn(`Unable to read folders in ${this.untitledWorkspacesHome} (${error}).`); + } + } + + return untitledWorkspaces; + } + + async enterWorkspace(window: ICodeWindow, windows: ICodeWindow[], path: URI): Promise { + if (!window || !window.win || !window.isReady) { + return null; // return early if the window is not ready or disposed + } + + const isValid = await this.isValidTargetWorkspacePath(window, windows, path); + if (!isValid) { + return null; // return early if the workspace is not valid + } + + const result = this.doEnterWorkspace(window, getWorkspaceIdentifier(path)); + if (!result) { + return null; + } + + // Emit as event + this._onWorkspaceEntered.fire({ window, workspace: result.workspace }); + + return result; + } + + private async isValidTargetWorkspacePath(window: ICodeWindow, windows: ICodeWindow[], workspacePath?: URI): Promise { + if (!workspacePath) { + return true; + } + + if (isWorkspaceIdentifier(window.openedWorkspace) && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.configPath, workspacePath)) { + return false; // window is already opened on a workspace with that path + } + + // Prevent overwriting a workspace that is currently opened in another window + if (findWindowOnWorkspaceOrFolder(windows, workspacePath)) { + const options: MessageBoxOptions = { + title: product.nameLong, + type: 'info', + buttons: [localize('ok', "OK")], + message: localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(workspacePath)), + detail: localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again."), + noLink: true + }; + + await this.dialogMainService.showMessageBox(options, withNullAsUndefined(BrowserWindow.getFocusedWindow())); + + return false; + } + + return true; // OK + } + + private doEnterWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): IEnterWorkspaceResult | null { + if (!window.config) { + return null; + } + + window.focus(); + + // Register window for backups and migrate current backups over + let backupPath: string | undefined; + if (!window.config.extensionDevelopmentPath) { + backupPath = this.backupMainService.registerWorkspaceBackupSync({ workspace, remoteAuthority: window.remoteAuthority }, window.config.backupPath); + } + + // if the window was opened on an untitled workspace, delete it. + if (isWorkspaceIdentifier(window.openedWorkspace) && this.isUntitledWorkspace(window.openedWorkspace)) { + this.deleteUntitledWorkspaceSync(window.openedWorkspace); + } + + // Update window configuration properly based on transition to workspace + window.config.workspace = workspace; + window.config.backupPath = backupPath; + + return { workspace, backupPath }; + } +} + +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// NOTE: DO NOT CHANGE. IDENTIFIERS HAVE TO REMAIN STABLE +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +export function getWorkspaceIdentifier(configPath: URI): IWorkspaceIdentifier { + + function getWorkspaceId(): string { + let configPathStr = configPath.scheme === Schemas.file ? originalFSPath(configPath) : configPath.toString(); + if (!isLinux) { + configPathStr = configPathStr.toLowerCase(); // sanitize for platform file system + } + + return createHash('md5').update(configPathStr).digest('hex'); + } + + return { + id: getWorkspaceId(), + configPath + }; +} + +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// NOTE: DO NOT CHANGE. IDENTIFIERS HAVE TO REMAIN STABLE +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +export function getSingleFolderWorkspaceIdentifier(folderUri: URI): ISingleFolderWorkspaceIdentifier | undefined; +export function getSingleFolderWorkspaceIdentifier(folderUri: URI, folderStat: Stats): ISingleFolderWorkspaceIdentifier; +export function getSingleFolderWorkspaceIdentifier(folderUri: URI, folderStat?: Stats): ISingleFolderWorkspaceIdentifier | undefined { + + function getFolderId(): string | undefined { + + // Remote: produce a hash from the entire URI + if (folderUri.scheme !== Schemas.file) { + return createHash('md5').update(folderUri.toString()).digest('hex'); + } + + // Local: produce a hash from the path and include creation time as salt + if (!folderStat) { + try { + folderStat = statSync(folderUri.fsPath); + } catch (error) { + return undefined; // folder does not exist + } + } + + let ctime: number | undefined; + if (isLinux) { + ctime = folderStat.ino; // Linux: birthtime is ctime, so we cannot use it! We use the ino instead! + } else if (isMacintosh) { + ctime = folderStat.birthtime.getTime(); // macOS: birthtime is fine to use as is + } else if (isWindows) { + if (typeof folderStat.birthtimeMs === 'number') { + ctime = Math.floor(folderStat.birthtimeMs); // Windows: fix precision issue in node.js 8.x to get 7.x results (see https://github.com/nodejs/node/issues/19897) + } else { + ctime = folderStat.birthtime.getTime(); + } + } + + // we use the ctime as extra salt to the ID so that we catch the case of a folder getting + // deleted and recreated. in that case we do not want to carry over previous state + return createHash('md5').update(folderUri.fsPath).update(ctime ? String(ctime) : '').digest('hex'); + } + + const folderId = getFolderId(); + if (typeof folderId === 'string') { + return { + id: folderId, + uri: folderUri + }; + } + + return undefined; // invalid folder +} diff --git a/src/vs/platform/workspaces/electron-main/workspacesService.ts b/src/vs/platform/workspaces/electron-main/workspacesService.ts deleted file mode 100644 index 8d1d22d9..00000000 --- a/src/vs/platform/workspaces/electron-main/workspacesService.ts +++ /dev/null @@ -1,81 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AddFirstParameterToFunctions } from 'vs/base/common/types'; -import { IWorkspacesService, IEnterWorkspaceResult, IWorkspaceFolderCreationData, IWorkspaceIdentifier, IRecentlyOpened, IRecent } from 'vs/platform/workspaces/common/workspaces'; -import { URI } from 'vs/base/common/uri'; -import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; -import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; -import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; - -export class WorkspacesService implements AddFirstParameterToFunctions /* only methods, not events */, number /* window ID */> { - - declare readonly _serviceBrand: undefined; - - constructor( - @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, - @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - @IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService, - @IBackupMainService private readonly backupMainService: IBackupMainService - ) { - } - - //#region Workspace Management - - async enterWorkspace(windowId: number, path: URI): Promise { - const window = this.windowsMainService.getWindowById(windowId); - if (window) { - return this.workspacesMainService.enterWorkspace(window, this.windowsMainService.getWindows(), path); - } - - return null; - } - - createUntitledWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise { - return this.workspacesMainService.createUntitledWorkspace(folders, remoteAuthority); - } - - deleteUntitledWorkspace(windowId: number, workspace: IWorkspaceIdentifier): Promise { - return this.workspacesMainService.deleteUntitledWorkspace(workspace); - } - - getWorkspaceIdentifier(windowId: number, workspacePath: URI): Promise { - return this.workspacesMainService.getWorkspaceIdentifier(workspacePath); - } - - //#endregion - - //#region Workspaces History - - readonly onRecentlyOpenedChange = this.workspacesHistoryMainService.onRecentlyOpenedChange; - - async getRecentlyOpened(windowId: number): Promise { - return this.workspacesHistoryMainService.getRecentlyOpened(this.windowsMainService.getWindowById(windowId)); - } - - async addRecentlyOpened(windowId: number, recents: IRecent[]): Promise { - return this.workspacesHistoryMainService.addRecentlyOpened(recents); - } - - async removeRecentlyOpened(windowId: number, paths: URI[]): Promise { - return this.workspacesHistoryMainService.removeRecentlyOpened(paths); - } - - async clearRecentlyOpened(windowId: number): Promise { - return this.workspacesHistoryMainService.clearRecentlyOpened(); - } - - //#endregion - - - //#region Dirty Workspaces - - async getDirtyWorkspaces(): Promise> { - return this.backupMainService.getDirtyWorkspaces(); - } - - //#endregion -} diff --git a/src/vs/platform/workspaces/test/common/workspaces.test.ts b/src/vs/platform/workspaces/test/common/workspaces.test.ts new file mode 100644 index 00000000..2c0a7565 --- /dev/null +++ b/src/vs/platform/workspaces/test/common/workspaces.test.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { URI } from 'vs/base/common/uri'; +import { hasWorkspaceFileExtension, toWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; + +suite('Workspaces', () => { + test('hasWorkspaceFileExtension', () => { + assert.strictEqual(hasWorkspaceFileExtension('something'), false); + assert.strictEqual(hasWorkspaceFileExtension('something.code-workspace'), true); + }); + + test('toWorkspaceIdentifier', () => { + let identifier = toWorkspaceIdentifier({ id: 'id', folders: [] }); + assert.ok(!identifier); + assert.ok(!isSingleFolderWorkspaceIdentifier(identifier)); + assert.ok(!isWorkspaceIdentifier(identifier)); + + identifier = toWorkspaceIdentifier({ id: 'id', folders: [{ index: 0, name: 'test', toResource: () => URI.file('test'), uri: URI.file('test') }] }); + assert.ok(identifier); + assert.ok(isSingleFolderWorkspaceIdentifier(identifier)); + assert.ok(!isWorkspaceIdentifier(identifier)); + + identifier = toWorkspaceIdentifier({ id: 'id', configuration: URI.file('test.code-workspace'), folders: [] }); + assert.ok(identifier); + assert.ok(!isSingleFolderWorkspaceIdentifier(identifier)); + assert.ok(isWorkspaceIdentifier(identifier)); + }); +}); diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts index c1637320..6f502744 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts @@ -17,25 +17,25 @@ function toWorkspace(uri: URI): IWorkspaceIdentifier { }; } function assertEqualURI(u1: URI | undefined, u2: URI | undefined, message?: string): void { - assert.equal(u1 && u1.toString(), u2 && u2.toString(), message); + assert.strictEqual(u1 && u1.toString(), u2 && u2.toString(), message); } function assertEqualWorkspace(w1: IWorkspaceIdentifier | undefined, w2: IWorkspaceIdentifier | undefined, message?: string): void { if (!w1 || !w2) { - assert.equal(w1, w2, message); + assert.strictEqual(w1, w2, message); return; } - assert.equal(w1.id, w2.id, message); + assert.strictEqual(w1.id, w2.id, message); assertEqualURI(w1.configPath, w2.configPath, message); } function assertEqualRecentlyOpened(actual: IRecentlyOpened, expected: IRecentlyOpened, message?: string) { - assert.equal(actual.files.length, expected.files.length, message); + assert.strictEqual(actual.files.length, expected.files.length, message); for (let i = 0; i < actual.files.length; i++) { assertEqualURI(actual.files[i].fileUri, expected.files[i].fileUri, message); - assert.equal(actual.files[i].label, expected.files[i].label); + assert.strictEqual(actual.files[i].label, expected.files[i].label); } - assert.equal(actual.workspaces.length, expected.workspaces.length, message); + assert.strictEqual(actual.workspaces.length, expected.workspaces.length, message); for (let i = 0; i < actual.workspaces.length; i++) { let expectedRecent = expected.workspaces[i]; let actualRecent = actual.workspaces[i]; @@ -44,7 +44,7 @@ function assertEqualRecentlyOpened(actual: IRecentlyOpened, expected: IRecentlyO } else { assertEqualWorkspace(actualRecent.workspace, (expectedRecent).workspace, message); } - assert.equal(actualRecent.label, expectedRecent.label); + assert.strictEqual(actualRecent.label, expectedRecent.label); } } @@ -129,8 +129,5 @@ suite('History Storage', () => { }; assertEqualRecentlyOpened(windowsState, expected, 'v1_33'); - }); - - }); diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts similarity index 77% rename from src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts rename to src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts index 07304aa3..2cbc34f4 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts @@ -10,15 +10,15 @@ import * as path from 'vs/base/common/path'; import * as pfs from 'vs/base/node/pfs'; import { EnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; -import { WorkspacesMainService, IStoredWorkspace } from 'vs/platform/workspaces/electron-main/workspacesMainService'; +import { WorkspacesManagementMainService, IStoredWorkspace, getSingleFolderWorkspaceIdentifier, getWorkspaceIdentifier } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { WORKSPACE_EXTENSION, IRawFileWorkspaceFolder, IWorkspaceFolderCreationData, IRawUriWorkspaceFolder, rewriteWorkspaceFileForNewLocation, IWorkspaceIdentifier, IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; import { NullLogService } from 'vs/platform/log/common/log'; import { URI } from 'vs/base/common/uri'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { isWindows } from 'vs/base/common/platform'; import { normalizeDriveLetter } from 'vs/base/common/labels'; -import { dirname, joinPath } from 'vs/base/common/resources'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { dirname, extUriBiasedIgnorePathCase, joinPath } from 'vs/base/common/resources'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { IBackupMainService, IWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup'; import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; @@ -104,15 +104,7 @@ export class TestBackupMainService implements IBackupMainService { } } -suite('WorkspacesMainService', () => { - const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'workspacesservice'); - const untitledWorkspacesHomePath = path.join(parentDir, 'Workspaces'); - - class TestEnvironmentService extends EnvironmentMainService { - get untitledWorkspacesHome(): URI { - return URI.file(untitledWorkspacesHomePath); - } - } +suite('WorkspacesManagementMainService', () => { function createUntitledWorkspace(folders: string[], names?: string[]) { return service.createUntitledWorkspace(folders.map((folder, index) => ({ uri: URI.file(folder), name: names ? names[index] : undefined } as IWorkspaceFolderCreationData))); @@ -138,22 +130,31 @@ suite('WorkspacesMainService', () => { return service.createUntitledWorkspaceSync(folders.map((folder, index) => ({ uri: URI.file(folder), name: names ? names[index] : undefined } as IWorkspaceFolderCreationData))); } - const environmentService = new TestEnvironmentService(parseArgs(process.argv, OPTIONS)); - const logService = new NullLogService(); - - let service: WorkspacesMainService; + let testDir: string; + let untitledWorkspacesHomePath: string; + let environmentService: EnvironmentMainService; + let service: WorkspacesManagementMainService; setup(async () => { - service = new WorkspacesMainService(environmentService, logService, new TestBackupMainService(), new TestDialogMainService()); + testDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'workspacesmanagementmainservice'); + untitledWorkspacesHomePath = path.join(testDir, 'Workspaces'); - // Delete any existing backups completely and then re-create it. - await pfs.rimraf(untitledWorkspacesHomePath, pfs.RimRafMode.MOVE); + environmentService = new class TestEnvironmentService extends EnvironmentMainService { + constructor() { + super(parseArgs(process.argv, OPTIONS)); + } + get untitledWorkspacesHome(): URI { + return URI.file(untitledWorkspacesHomePath); + } + }; + + service = new WorkspacesManagementMainService(environmentService, new NullLogService(), new TestBackupMainService(), new TestDialogMainService()); return pfs.mkdirp(untitledWorkspacesHomePath); }); teardown(() => { - return pfs.rimraf(untitledWorkspacesHomePath, pfs.RimRafMode.MOVE); + return pfs.rimraf(testDir); }); function assertPathEquals(p1: string, p2: string): void { @@ -162,11 +163,11 @@ suite('WorkspacesMainService', () => { p2 = normalizeDriveLetter(p2); } - assert.equal(p1, p2); + assert.strictEqual(p1, p2); } function assertEqualURI(u1: URI, u2: URI): void { - assert.equal(u1.toString(), u2.toString()); + assert.strictEqual(u1.toString(), u2.toString()); } test('createWorkspace (folders)', async () => { @@ -176,7 +177,7 @@ suite('WorkspacesMainService', () => { assert.ok(service.isUntitledWorkspace(workspace)); const ws = (JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace); - assert.equal(ws.folders.length, 2); + assert.strictEqual(ws.folders.length, 2); assertPathEquals((ws.folders[0]).path, process.cwd()); assertPathEquals((ws.folders[1]).path, os.tmpdir()); assert.ok(!(ws.folders[0]).name); @@ -190,11 +191,11 @@ suite('WorkspacesMainService', () => { assert.ok(service.isUntitledWorkspace(workspace)); const ws = (JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace); - assert.equal(ws.folders.length, 2); + assert.strictEqual(ws.folders.length, 2); assertPathEquals((ws.folders[0]).path, process.cwd()); assertPathEquals((ws.folders[1]).path, os.tmpdir()); - assert.equal((ws.folders[0]).name, 'currentworkingdirectory'); - assert.equal((ws.folders[1]).name, 'tempdir'); + assert.strictEqual((ws.folders[0]).name, 'currentworkingdirectory'); + assert.strictEqual((ws.folders[1]).name, 'tempdir'); }); test('createUntitledWorkspace (folders as other resource URIs)', async () => { @@ -207,12 +208,12 @@ suite('WorkspacesMainService', () => { assert.ok(service.isUntitledWorkspace(workspace)); const ws = (JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace); - assert.equal(ws.folders.length, 2); - assert.equal((ws.folders[0]).uri, folder1URI.toString(true)); - assert.equal((ws.folders[1]).uri, folder2URI.toString(true)); + assert.strictEqual(ws.folders.length, 2); + assert.strictEqual((ws.folders[0]).uri, folder1URI.toString(true)); + assert.strictEqual((ws.folders[1]).uri, folder2URI.toString(true)); assert.ok(!(ws.folders[0]).name); assert.ok(!(ws.folders[1]).name); - assert.equal(ws.remoteAuthority, 'server'); + assert.strictEqual(ws.remoteAuthority, 'server'); }); test('createWorkspaceSync (folders)', () => { @@ -222,7 +223,7 @@ suite('WorkspacesMainService', () => { assert.ok(service.isUntitledWorkspace(workspace)); const ws = JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace; - assert.equal(ws.folders.length, 2); + assert.strictEqual(ws.folders.length, 2); assertPathEquals((ws.folders[0]).path, process.cwd()); assertPathEquals((ws.folders[1]).path, os.tmpdir()); @@ -237,12 +238,12 @@ suite('WorkspacesMainService', () => { assert.ok(service.isUntitledWorkspace(workspace)); const ws = JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace; - assert.equal(ws.folders.length, 2); + assert.strictEqual(ws.folders.length, 2); assertPathEquals((ws.folders[0]).path, process.cwd()); assertPathEquals((ws.folders[1]).path, os.tmpdir()); - assert.equal((ws.folders[0]).name, 'currentworkingdirectory'); - assert.equal((ws.folders[1]).name, 'tempdir'); + assert.strictEqual((ws.folders[0]).name, 'currentworkingdirectory'); + assert.strictEqual((ws.folders[1]).name, 'tempdir'); }); test('createUntitledWorkspaceSync (folders as other resource URIs)', () => { @@ -255,9 +256,9 @@ suite('WorkspacesMainService', () => { assert.ok(service.isUntitledWorkspace(workspace)); const ws = JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace; - assert.equal(ws.folders.length, 2); - assert.equal((ws.folders[0]).uri, folder1URI.toString(true)); - assert.equal((ws.folders[1]).uri, folder2URI.toString(true)); + assert.strictEqual(ws.folders.length, 2); + assert.strictEqual((ws.folders[0]).uri, folder1URI.toString(true)); + assert.strictEqual((ws.folders[1]).uri, folder2URI.toString(true)); assert.ok(!(ws.folders[0]).name); assert.ok(!(ws.folders[1]).name); @@ -273,7 +274,7 @@ suite('WorkspacesMainService', () => { workspace.configPath = URI.file(newPath); const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); - assert.equal(2, resolved!.folders.length); + assert.strictEqual(2, resolved!.folders.length); assertEqualURI(resolved!.configPath, workspace.configPath); assert.ok(resolved!.id); fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ something: 'something' })); // invalid workspace @@ -325,39 +326,39 @@ suite('WorkspacesMainService', () => { let origConfigPath = URI.file(firstConfigPath); let workspaceConfigPath = URI.file(path.join(tmpDir, 'inside', 'myworkspace1.code-workspace')); - let newContent = rewriteWorkspaceFileForNewLocation(origContent, origConfigPath, false, workspaceConfigPath); + let newContent = rewriteWorkspaceFileForNewLocation(origContent, origConfigPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); let ws = (JSON.parse(newContent) as IStoredWorkspace); - assert.equal(ws.folders.length, 3); + assert.strictEqual(ws.folders.length, 3); assertPathEquals((ws.folders[0]).path, folder1); // absolute path because outside of tmpdir assertPathEquals((ws.folders[1]).path, '.'); assertPathEquals((ws.folders[2]).path, 'somefolder'); origConfigPath = workspaceConfigPath; workspaceConfigPath = URI.file(path.join(tmpDir, 'myworkspace2.code-workspace')); - newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath); + newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); ws = (JSON.parse(newContent) as IStoredWorkspace); - assert.equal(ws.folders.length, 3); + assert.strictEqual(ws.folders.length, 3); assertPathEquals((ws.folders[0]).path, folder1); assertPathEquals((ws.folders[1]).path, 'inside'); assertPathEquals((ws.folders[2]).path, isWindows ? 'inside\\somefolder' : 'inside/somefolder'); origConfigPath = workspaceConfigPath; workspaceConfigPath = URI.file(path.join(tmpDir, 'other', 'myworkspace2.code-workspace')); - newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath); + newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); ws = (JSON.parse(newContent) as IStoredWorkspace); - assert.equal(ws.folders.length, 3); + assert.strictEqual(ws.folders.length, 3); assertPathEquals((ws.folders[0]).path, folder1); assertPathEquals((ws.folders[1]).path, isWindows ? '..\\inside' : '../inside'); assertPathEquals((ws.folders[2]).path, isWindows ? '..\\inside\\somefolder' : '../inside/somefolder'); origConfigPath = workspaceConfigPath; workspaceConfigPath = URI.parse('foo://foo/bar/myworkspace2.code-workspace'); - newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath); + newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); ws = (JSON.parse(newContent) as IStoredWorkspace); - assert.equal(ws.folders.length, 3); - assert.equal((ws.folders[0]).uri, URI.file(folder1).toString(true)); - assert.equal((ws.folders[1]).uri, URI.file(tmpInsideDir).toString(true)); - assert.equal((ws.folders[2]).uri, URI.file(path.join(tmpInsideDir, 'somefolder')).toString(true)); + assert.strictEqual(ws.folders.length, 3); + assert.strictEqual((ws.folders[0]).uri, URI.file(folder1).toString(true)); + assert.strictEqual((ws.folders[1]).uri, URI.file(tmpInsideDir).toString(true)); + assert.strictEqual((ws.folders[2]).uri, URI.file(path.join(tmpInsideDir, 'somefolder')).toString(true)); fs.unlinkSync(firstConfigPath); }); @@ -369,8 +370,8 @@ suite('WorkspacesMainService', () => { let origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); origContent = `// this is a comment\n${origContent}`; - let newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath); - assert.equal(0, newContent.indexOf('// this is a comment')); + let newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); + assert.strictEqual(0, newContent.indexOf('// this is a comment')); service.deleteUntitledWorkspaceSync(workspace); }); @@ -381,26 +382,22 @@ suite('WorkspacesMainService', () => { let origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); origContent = origContent.replace(/[\\]/g, '/'); // convert backslash to slash - const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath); + const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); const ws = (JSON.parse(newContent) as IStoredWorkspace); assert.ok(ws.folders.every(f => (f).path.indexOf('\\') < 0)); service.deleteUntitledWorkspaceSync(workspace); }); - test.skip('rewriteWorkspaceFileForNewLocation (unc paths)', async () => { - if (!isWindows) { - return Promise.resolve(); - } - + (!isWindows ? test.skip : test)('rewriteWorkspaceFileForNewLocation (unc paths)', async () => { const workspaceLocation = path.join(os.tmpdir(), 'wsloc'); const folder1Location = 'x:\\foo'; const folder2Location = '\\\\server\\share2\\some\\path'; - const folder3Location = path.join(os.tmpdir(), 'wsloc', 'inner', 'more'); + const folder3Location = path.join(workspaceLocation, 'inner', 'more'); const workspace = await createUntitledWorkspace([folder1Location, folder2Location, folder3Location]); const workspaceConfigPath = URI.file(path.join(workspaceLocation, `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`)); let origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); - const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath); + const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, true, workspaceConfigPath, extUriBiasedIgnorePathCase); const ws = (JSON.parse(newContent) as IStoredWorkspace); assertPathEquals((ws.folders[0]).path, folder1Location); assertPathEquals((ws.folders[1]).path, folder2Location); @@ -422,17 +419,15 @@ suite('WorkspacesMainService', () => { }); test('getUntitledWorkspaceSync', async function () { - this.retries(3); - let untitled = service.getUntitledWorkspacesSync(); - assert.equal(untitled.length, 0); + assert.strictEqual(untitled.length, 0); const untitledOne = await createUntitledWorkspace([process.cwd(), os.tmpdir()]); assert.ok(fs.existsSync(untitledOne.configPath.fsPath)); untitled = service.getUntitledWorkspacesSync(); - assert.equal(1, untitled.length); - assert.equal(untitledOne.id, untitled[0].workspace.id); + assert.strictEqual(1, untitled.length); + assert.strictEqual(untitledOne.id, untitled[0].workspace.id); const untitledTwo = await createUntitledWorkspace([os.tmpdir(), process.cwd()]); assert.ok(fs.existsSync(untitledTwo.configPath.fsPath)); @@ -444,14 +439,50 @@ suite('WorkspacesMainService', () => { if (untitled.length === 1) { assert.fail(`Unexpected workspaces count of 1 (expected 2), all workspaces:\n ${fs.readdirSync(untitledHome.fsPath).map(name => fs.readFileSync(joinPath(untitledHome, name, 'workspace.json').fsPath, 'utf8'))}, before getUntitledWorkspacesSync: ${beforeGettingUntitledWorkspaces}`); } - assert.equal(2, untitled.length); + assert.strictEqual(2, untitled.length); service.deleteUntitledWorkspaceSync(untitledOne); untitled = service.getUntitledWorkspacesSync(); - assert.equal(1, untitled.length); + assert.strictEqual(1, untitled.length); service.deleteUntitledWorkspaceSync(untitledTwo); untitled = service.getUntitledWorkspacesSync(); - assert.equal(0, untitled.length); + assert.strictEqual(0, untitled.length); + }); + + test('getSingleWorkspaceIdentifier', async function () { + const nonLocalUri = URI.parse('myscheme://server/work/p/f1'); + const nonLocalUriId = getSingleFolderWorkspaceIdentifier(nonLocalUri); + assert.ok(nonLocalUriId?.id); + + const localNonExistingUri = URI.file(path.join(testDir, 'f1')); + const localNonExistingUriId = getSingleFolderWorkspaceIdentifier(localNonExistingUri); + assert.ok(!localNonExistingUriId); + + fs.mkdirSync(path.join(testDir, 'f1')); + + const localExistingUri = URI.file(path.join(testDir, 'f1')); + const localExistingUriId = getSingleFolderWorkspaceIdentifier(localExistingUri); + assert.ok(localExistingUriId?.id); + }); + + test('workspace identifiers are stable', function () { + + // workspace identifier (local) + assert.strictEqual(getWorkspaceIdentifier(URI.file('/hello/test')).id, isWindows /* slash vs backslash */ ? '9f3efb614e2cd7924e4b8076e6c72233' : 'e36736311be12ff6d695feefe415b3e8'); + + // single folder identifier (local) + const fakeStat = { + ino: 1611312115129, + birthtimeMs: 1611312115129, + birthtime: new Date(1611312115129) + }; + assert.strictEqual(getSingleFolderWorkspaceIdentifier(URI.file('/hello/test'), fakeStat as fs.Stats)?.id, isWindows /* slash vs backslash */ ? '9a8441e897e5174fa388bc7ef8f7a710' : '1d726b3d516dc2a6d343abf4797eaaef'); + + // workspace identifier (remote) + assert.strictEqual(getWorkspaceIdentifier(URI.parse('vscode-remote:/hello/test')).id, '786de4f224d57691f218dc7f31ee2ee3'); + + // single folder identifier (remote) + assert.strictEqual(getSingleFolderWorkspaceIdentifier(URI.parse('vscode-remote:/hello/test'))?.id, '786de4f224d57691f218dc7f31ee2ee3'); }); }); diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index e1c2dd30..7c5c6828 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -1450,6 +1450,21 @@ declare module 'vscode' { dispose(): void; } + /** + * An error type that should be used to signal cancellation of an operation. + * + * This type can be used in response to a [cancellation token](#CancellationToken) + * being cancelled or when an operation is being cancelled by the + * executor of that operation. + */ + export class CancellationError extends Error { + + /** + * Creates a new cancellation error. + */ + constructor(); + } + /** * Represents a type which can release resources, such * as event listening or a timer. @@ -1700,8 +1715,8 @@ declare module 'vscode' { /** * Options to configure the behaviour of a file open dialog. * - * * Note 1: A dialog can select files, folders, or both. This is not true for Windows - * which enforces to open either files or folder, but *not both*. + * * Note 1: On Windows and Linux, a file dialog cannot be both a file selector and a folder selector, so if you + * set both `canSelectFiles` and `canSelectFolders` to `true` on these platforms, a folder selector will be shown. * * Note 2: Explicitly setting `canSelectFiles` and `canSelectFolders` to `false` is futile * and the editor then silently adjusts the options to select files. */ @@ -3870,8 +3885,8 @@ declare module 'vscode' { * * Note that `sortText` is only used for the initial ordering of completion * items. When having a leading word (prefix) ordering is based on how - * well completion match that prefix and the initial ordering is only used - * when completions match equal. The prefix is defined by the + * well completions match that prefix and the initial ordering is only used + * when completions match equally well. The prefix is defined by the * [`range`](#CompletionItem.range)-property and can therefore be different * for each completion. */ @@ -3884,7 +3899,6 @@ declare module 'vscode' { * * Note that the filter text is matched against the leading word (prefix) which is defined * by the [`range`](#CompletionItem.range)-property. - * prefix. */ filterText?: string; @@ -4683,6 +4697,10 @@ declare module 'vscode' { * This rule will only execute if the text after the cursor matches this regular expression. */ afterText?: RegExp; + /** + * This rule will only execute if the text above the current line matches this regular expression. + */ + previousLineText?: RegExp; /** * The action to execute. */ @@ -5173,9 +5191,9 @@ declare module 'vscode' { set(uri: Uri, diagnostics: ReadonlyArray | undefined): void; /** - * Replace all entries in this collection. + * Replace diagnostics for multiple resources in this collection. * - * Diagnostics of multiple tuples of the same uri will be merged, e.g + * _Note_ that multiple tuples of the same uri will be merged, e.g * `[[file1, [d1]], [file1, [d2]]]` is equivalent to `[[file1, [d1, d2]]]`. * If a diagnostics item is `undefined` as in `[file1, undefined]` * all previous but not subsequent diagnostics are removed. @@ -5419,6 +5437,18 @@ declare module 'vscode' { */ color: string | ThemeColor | undefined; + /** + * The background color for this entry. + * + * *Note*: only `new ThemeColor('statusBarItem.errorBackground')` is + * supported for now. More background colors may be supported in the + * future. + * + * *Note*: when a background color is set, the statusbar may override + * the `color` choice to ensure the entry is readable in all themes. + */ + backgroundColor: ThemeColor | undefined; + /** * [`Command`](#Command) or identifier of a command to run on click. * @@ -5795,6 +5825,11 @@ declare module 'vscode' { setKeysForSync(keys: string[]): void; }; + /** + * A storage utility for secrets. + */ + readonly secrets: SecretStorage; + /** * The uri of the directory containing the extension. */ @@ -5932,6 +5967,48 @@ declare module 'vscode' { update(key: string, value: any): Thenable; } + /** + * The event data that is fired when a secret is added or removed. + */ + export interface SecretStorageChangeEvent { + /** + * The key of the secret that has changed. + */ + readonly key: string; + } + + /** + * Represents a storage utility for secrets, information that is + * sensitive. + */ + export interface SecretStorage { + /** + * Retrieve a secret that was stored with key. Returns undefined if there + * is no password matching that key. + * @param key The key the secret was stored under. + * @returns The stored value or `undefined`. + */ + get(key: string): Thenable; + + /** + * Store a secret under a given key. + * @param key The key to store the secret under. + * @param value The secret. + */ + store(key: string, value: string): Thenable; + + /** + * Remove a secret from storage. + * @param key The key the secret was stored under. + */ + delete(key: string): Thenable; + + /** + * Fires when a secret is stored or deleted. + */ + onDidChange: Event; + } + /** * Represents a color theme kind. */ @@ -7732,7 +7809,7 @@ declare module 'vscode' { * your extension should first check to see if any backups exist for the resource. If there is a backup, your * extension should load the file contents from there instead of from the resource in the workspace. * - * `backup` is triggered approximately one second after the the user stops editing the document. If the user + * `backup` is triggered approximately one second after the user stops editing the document. If the user * rapidly edits the document, `backup` will not be invoked until the editing stops. * * `backup` is not invoked when `auto save` is enabled (since auto save already persists the resource). @@ -8855,7 +8932,8 @@ declare module 'vscode' { getParent?(element: T): ProviderResult; /** - * Called only on hover to resolve the [TreeItem](#TreeItem.tooltip) property if it is undefined. + * Called on hover to resolve the [TreeItem](#TreeItem.tooltip) property if it is undefined. + * Called on tree item click/open to resolve the [TreeItem](#TreeItem.command) property if it is undefined. * Only properties that were undefined can be resolved in `resolveTreeItem`. * Functionality may be expanded later to include being called to resolve other missing * properties on selection and/or on open. @@ -8865,15 +8943,16 @@ declare module 'vscode' { * onDidChangeTreeData should not be triggered from within resolveTreeItem. * * *Note* that this function is called when tree items are already showing in the UI. - * Because of that, no property that changes the presentation (label, description, command, etc.) + * Because of that, no property that changes the presentation (label, description, etc.) * can be changed. * - * @param element The object associated with the TreeItem * @param item Undefined properties of `item` should be set then `item` should be returned. + * @param element The object associated with the TreeItem. + * @param token A cancellation token. * @return The resolved tree item or a thenable that resolves to such. It is OK to return the given * `item`. When no result is returned, the given `item` will be used. */ - resolveTreeItem?(item: TreeItem, element: T): ProviderResult; + resolveTreeItem?(item: TreeItem, element: T, token: CancellationToken): ProviderResult; } export class TreeItem { @@ -9194,7 +9273,7 @@ declare module 'vscode' { * Implement to handle when the number of rows and columns that fit into the terminal panel * changes, for example when font size changes or when the panel is resized. The initial * state of a terminal's dimensions should be treated as `undefined` until this is triggered - * as the size of a terminal isn't know until it shows up in the user interface. + * as the size of a terminal isn't known until it shows up in the user interface. * * When dimensions are overridden by * [onDidOverrideDimensions](#Pseudoterminal.onDidOverrideDimensions), `setDimensions` will @@ -11865,8 +11944,6 @@ declare module 'vscode' { export const onDidChange: Event; } - //#region Comments - /** * Collapsible state of a [comment thread](#CommentThread) */ @@ -12133,7 +12210,7 @@ declare module 'vscode' { /** * Optional reaction handler for creating and deleting reactions on a [comment](#Comment). */ - reactionHandler?: (comment: Comment, reaction: CommentReaction) => Promise; + reactionHandler?: (comment: Comment, reaction: CommentReaction) => Thenable; /** * Dispose this comment controller. diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 19e8d8f1..0aa96cf7 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -16,7 +16,7 @@ declare module 'vscode' { - // #region auth provider: https://github.com/microsoft/vscode/issues/88309 + //#region auth provider: https://github.com/microsoft/vscode/issues/88309 /** * An [event](#Event) which fires when an [AuthenticationProvider](#AuthenticationProvider) is added or removed. @@ -54,28 +54,9 @@ declare module 'vscode' { } /** - * **WARNING** When writing an AuthenticationProvider, `id` should be treated as part of your extension's - * API, changing it is a breaking change for all extensions relying on the provider. The id is - * treated case-sensitively. + * A provider for performing authentication to a service. */ export interface AuthenticationProvider { - /** - * Used as an identifier for extensions trying to work with a particular - * provider: 'microsoft', 'github', etc. id must be unique, registering - * another provider with the same id will fail. - */ - readonly id: string; - - /** - * The human-readable name of the provider. - */ - readonly label: string; - - /** - * Whether it is possible to be signed into multiple accounts at once with this provider - */ - readonly supportsMultipleAccounts: boolean; - /** * An [event](#Event) which fires when the array of sessions has changed, or data * within a session has changed. @@ -85,31 +66,48 @@ declare module 'vscode' { /** * Returns an array of current sessions. */ + // eslint-disable-next-line vscode-dts-provider-naming getSessions(): Thenable>; /** * Prompts a user to login. */ + // eslint-disable-next-line vscode-dts-provider-naming login(scopes: string[]): Thenable; /** * Removes the session corresponding to session id. * @param sessionId The session id to log out of */ + // eslint-disable-next-line vscode-dts-provider-naming logout(sessionId: string): Thenable; } + /** + * Options for creating an [AuthenticationProvider](#AuthentcationProvider). + */ + export interface AuthenticationProviderOptions { + /** + * Whether it is possible to be signed into multiple accounts at once with this provider. + * If not specified, will default to false. + */ + readonly supportsMultipleAccounts?: boolean; + } + export namespace authentication { /** * Register an authentication provider. * * There can only be one provider per id and an error is being thrown when an id - * has already been used by another provider. + * has already been used by another provider. Ids are case-sensitive. * + * @param id The unique identifier of the provider. + * @param label The human-readable name of the provider. * @param provider The authentication provider provider. + * @params options Additional options for the provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ - export function registerAuthenticationProvider(provider: AuthenticationProvider): Disposable; + export function registerAuthenticationProvider(id: string, label: string, provider: AuthenticationProvider, options?: AuthenticationProviderOptions): Disposable; /** * @deprecated - getSession should now trigger extension activation. @@ -117,63 +115,32 @@ declare module 'vscode' { */ export const onDidChangeAuthenticationProviders: Event; - /** - * @deprecated - * The ids of the currently registered authentication providers. - * @returns An array of the ids of authentication providers that are currently registered. - */ - export function getProviderIds(): Thenable>; - - /** - * @deprecated - * An array of the ids of authentication providers that are currently registered. - */ - export const providerIds: ReadonlyArray; - /** * An array of the information of authentication providers that are currently registered. */ export const providers: ReadonlyArray; /** - * @deprecated * Logout of a specific session. * @param providerId The id of the provider to use * @param sessionId The session id to remove * provider */ export function logout(providerId: string, sessionId: string): Thenable; - - /** - * Retrieve a password that was stored with key. Returns undefined if there - * is no password matching that key. - * @param key The key the password was stored under. - */ - export function getPassword(key: string): Thenable; - - /** - * Store a password under a given key. - * @param key The key to store the password under - * @param value The password - */ - export function setPassword(key: string, value: string): Thenable; - - /** - * Remove a password from storage. - * @param key The key the password was stored under. - */ - export function deletePassword(key: string): Thenable; - - /** - * Fires when a password is set or deleted. - */ - export const onDidChangePassword: Event; } //#endregion + // eslint-disable-next-line vscode-dts-region-comments //#region @alexdima - resolvers + export interface MessageOptions { + /** + * Do not render a native message box. + */ + useCustom?: boolean; + } + export interface RemoteAuthorityResolverContext { resolveAttempt: number; } @@ -195,18 +162,20 @@ declare module 'vscode' { // The desired local port. If this port can't be used, then another will be chosen. localAddressPort?: number; label?: string; + public?: boolean; } export interface TunnelDescription { remoteAddress: { port: number, host: string; }; //The complete local address(ex. localhost:1234) localAddress: { port: number, host: string; } | string; + public?: boolean; } export interface Tunnel extends TunnelDescription { // Implementers of Tunnel should fire onDidDispose when dispose is called. onDidDispose: Event; - dispose(): void; + dispose(): void | Thenable; } /** @@ -251,10 +220,19 @@ declare module 'vscode' { */ tunnelFactory?: (tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions) => Thenable | undefined; - /** + /**p * Provides filtering for candidate ports. */ showCandidatePort?: (host: string, port: number, detail: string) => Thenable; + + /** + * Lets the resolver declare which tunnel factory features it supports. + * UNDER DISCUSSION! MAY CHANGE SOON. + */ + tunnelFeatures?: { + elevation: boolean; + public: boolean; + }; } export namespace workspace { @@ -751,6 +729,7 @@ declare module 'vscode' { //#endregion + // eslint-disable-next-line vscode-dts-region-comments //#region debug /** @@ -771,8 +750,7 @@ declare module 'vscode' { //#endregion - - + // eslint-disable-next-line vscode-dts-region-comments //#region @joaomoreno: SCM validation /** @@ -823,6 +801,7 @@ declare module 'vscode' { //#endregion + // eslint-disable-next-line vscode-dts-region-comments //#region @joaomoreno: SCM selected provider export interface SourceControl { @@ -898,6 +877,7 @@ declare module 'vscode' { //#endregion + // eslint-disable-next-line vscode-dts-region-comments //#region @jrieken -> exclusive document filters export interface DocumentFilter { @@ -906,18 +886,9 @@ declare module 'vscode' { //#endregion - //#region @alexdima - OnEnter enhancement - export interface OnEnterRule { - /** - * This rule will only execute if the text above the this line matches this regular expression. - */ - oneLineAboveText?: RegExp; - } - //#endregion - - //#region Tree View: https://github.com/microsoft/vscode/issues/61313 + //#region Tree View: https://github.com/microsoft/vscode/issues/61313 @alexr00 export interface TreeView extends Disposable { - reveal(element: T | undefined, options?: { select?: boolean, focus?: boolean, expand?: boolean | number }): Thenable; + reveal(element: T | undefined, options?: { select?: boolean, focus?: boolean, expand?: boolean | number; }): Thenable; } //#endregion @@ -1001,6 +972,7 @@ declare module 'vscode' { * * @return Thenable indicating that the webview editor has been moved. */ + // eslint-disable-next-line vscode-dts-provider-naming moveCustomTextEditor?(newDocument: TextDocument, existingWebviewPanel: WebviewPanel, token: CancellationToken): Thenable; } @@ -1017,7 +989,7 @@ declare module 'vscode' { //#endregion - //#region @rebornix: Notebook + //#region notebook https://github.com/microsoft/vscode/issues/106744 export enum CellKind { Markdown = 1, @@ -1055,7 +1027,7 @@ declare module 'vscode' { /** * Additional attributes of a cell metadata. */ - custom?: { [key: string]: any }; + custom?: { [key: string]: any; }; } export interface CellDisplayOutput { @@ -1179,7 +1151,7 @@ declare module 'vscode' { /** * Additional attributes of a cell metadata. */ - custom?: { [key: string]: any }; + custom?: { [key: string]: any; }; } export interface NotebookCell { @@ -1229,7 +1201,7 @@ declare module 'vscode' { /** * Additional attributes of the document metadata. */ - custom?: { [key: string]: any }; + custom?: { [key: string]: any; }; /** * The document's current run state @@ -1241,6 +1213,11 @@ declare module 'vscode' { * When false, insecure outputs like HTML, JavaScript, SVG will not be rendered. */ trusted?: boolean; + + /** + * Languages the document supports + */ + languages?: string[]; } export interface NotebookDocumentContentOptions { @@ -1286,7 +1263,7 @@ declare module 'vscode' { locationAt(positionOrRange: Position | Range): Location; positionAt(location: Location): Position; - contains(uri: Uri): boolean + contains(uri: Uri): boolean; } export interface WorkspaceEdit { @@ -1320,11 +1297,17 @@ declare module 'vscode' { * The range will always be revealed in the center of the viewport. */ InCenter = 1, + /** * If the range is outside the viewport, it will be revealed in the center of the viewport. * Otherwise, it will be revealed with as little scrolling as possible. */ InCenterIfOutsideViewport = 2, + + /** + * The range will always be revealed at the top of the viewport. + */ + AtTop = 3 } export interface NotebookEditor { @@ -1589,11 +1572,17 @@ declare module 'vscode' { * Content providers should always use [file system providers](#FileSystemProvider) to * resolve the raw content for `uri` as the resouce is not necessarily a file on disk. */ - openNotebook(uri: Uri, openContext: NotebookDocumentOpenContext): NotebookData | Promise; - resolveNotebook(document: NotebookDocument, webview: NotebookCommunication): Promise; - saveNotebook(document: NotebookDocument, cancellation: CancellationToken): Promise; - saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Promise; - backupNotebook(document: NotebookDocument, context: NotebookDocumentBackupContext, cancellation: CancellationToken): Promise; + // eslint-disable-next-line vscode-dts-provider-naming + openNotebook(uri: Uri, openContext: NotebookDocumentOpenContext): NotebookData | Thenable; + // eslint-disable-next-line vscode-dts-provider-naming + // eslint-disable-next-line vscode-dts-cancellation + resolveNotebook(document: NotebookDocument, webview: NotebookCommunication): Thenable; + // eslint-disable-next-line vscode-dts-provider-naming + saveNotebook(document: NotebookDocument, cancellation: CancellationToken): Thenable; + // eslint-disable-next-line vscode-dts-provider-naming + saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Thenable; + // eslint-disable-next-line vscode-dts-provider-naming + backupNotebook(document: NotebookDocument, context: NotebookDocumentBackupContext, cancellation: CancellationToken): Thenable; } export interface NotebookKernel { @@ -1609,7 +1598,7 @@ declare module 'vscode' { cancelAllCellsExecution(document: NotebookDocument): void; } - export type NotebookFilenamePattern = GlobPattern | { include: GlobPattern; exclude: GlobPattern }; + export type NotebookFilenamePattern = GlobPattern | { include: GlobPattern; exclude: GlobPattern; }; export interface NotebookDocumentFilter { viewType?: string | string[]; @@ -1692,7 +1681,7 @@ declare module 'vscode' { ): Disposable; export function createNotebookEditorDecorationType(options: NotebookDecorationRenderOptions): NotebookEditorDecorationType; - export function openNotebookDocument(uri: Uri, viewType?: string): Promise; + export function openNotebookDocument(uri: Uri, viewType?: string): Thenable; export const onDidOpenNotebookDocument: Event; export const onDidCloseNotebookDocument: Event; export const onDidSaveNotebookDocument: Event; @@ -1715,7 +1704,7 @@ declare module 'vscode' { */ export function createConcatTextDocument(notebook: NotebookDocument, selector?: DocumentSelector): NotebookConcatTextDocument; - export const onDidChangeActiveNotebookKernel: Event<{ document: NotebookDocument, kernel: NotebookKernel | undefined }>; + export const onDidChangeActiveNotebookKernel: Event<{ document: NotebookDocument, kernel: NotebookKernel | undefined; }>; /** * Creates a notebook cell status bar [item](#NotebookCellStatusBarItem). @@ -1736,7 +1725,7 @@ declare module 'vscode' { export const onDidChangeActiveNotebookEditor: Event; export const onDidChangeNotebookEditorSelection: Event; export const onDidChangeNotebookEditorVisibleRanges: Event; - export function showNotebookDocument(document: NotebookDocument, options?: NotebookDocumentShowOptions): Promise; + export function showNotebookDocument(document: NotebookDocument, options?: NotebookDocumentShowOptions): Thenable; } //#endregion @@ -1947,11 +1936,88 @@ declare module 'vscode' { } export namespace languages { - export function getTokenInformationAtPosition(document: TextDocument, position: Position): Promise; + export function getTokenInformationAtPosition(document: TextDocument, position: Position): Thenable; } //#endregion + //#region https://github.com/microsoft/vscode/issues/16221 + + export namespace languages { + /** + * Register a inline hints provider. + * + * Multiple providers can be registered for a language. In that case providers are asked in + * parallel and the results are merged. A failing provider (rejected promise or exception) will + * not cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider An inline hints provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerInlineHintsProvider(selector: DocumentSelector, provider: InlineHintsProvider): Disposable; + } + + /** + * Inline hint information. + */ + export class InlineHint { + /** + * The text of the hint. + */ + text: string; + /** + * The range of the hint. + */ + range: Range; + /** + * Tooltip when hover on the hint. + */ + description?: string | MarkdownString; + /** + * Whitespace before the hint. + */ + whitespaceBefore?: boolean; + /** + * Whitespace after the hint. + */ + whitespaceAfter?: boolean; + + /** + * Creates a new inline hint information object. + * + * @param text The text of the hint. + * @param range The range of the hint. + * @param hoverMessage Tooltip when hover on the hint. + * @param whitespaceBefore Whitespace before the hint. + * @param whitespaceAfter TWhitespace after the hint. + */ + constructor(text: string, range: Range, description?: string | MarkdownString, whitespaceBefore?: boolean, whitespaceAfter?: boolean); + } + + /** + * The inline hints provider interface defines the contract between extensions and + * the inline hints feature. + */ + export interface InlineHintsProvider { + + /** + * An optional event to signal that inline hints have changed. + * @see [EventEmitter](#EventEmitter) + */ + onDidChangeInlineHints?: Event; + + /** + * @param model The document in which the command was invoked. + * @param range The range for which line hints should be computed. + * @param token A cancellation token. + * + * @return A list of arguments labels or a thenable that resolves to such. + */ + provideInlineHints(model: TextDocument, range: Range, token: CancellationToken): ProviderResult; + } + //#endregion + //#region https://github.com/microsoft/vscode/issues/104436 export enum ExtensionRuntime { @@ -1971,7 +2037,6 @@ declare module 'vscode' { //#endregion - //#region https://github.com/microsoft/vscode/issues/102091 export interface TextDocument { @@ -2004,7 +2069,7 @@ declare module 'vscode' { * Runs tests with the given options. If no options are given, then * all tests are run. Returns the resulting test run. */ - export function runTests(options: TestRunOptions): Thenable; + export function runTests(options: TestRunOptions, cancellationToken?: CancellationToken): Thenable; /** * Returns an observer that retrieves tests in the given workspace folder. @@ -2015,13 +2080,31 @@ declare module 'vscode' { * Returns an observer that retrieves tests in the given text document. */ export function createDocumentTestObserver(document: TextDocument): TestObserver; + + /** + * The last or selected test run. Cleared when a new test run starts. + */ + export const testResults: TestResults | undefined; + + /** + * Event that fires when the testResults are updated. + */ + export const onDidChangeTestResults: Event; + } + + export interface TestResults { + /** + * The results from the latest test run. The array contains a snapshot of + * all tests involved in the run at the moment when it completed. + */ + readonly tests: ReadonlyArray | undefined; } export interface TestObserver { /** * List of tests returned by test provider for files in the workspace. */ - readonly tests: ReadonlyArray; + readonly tests: ReadonlyArray; /** * An event that fires when an existing test in the collection changes, or @@ -2051,23 +2134,23 @@ declare module 'vscode' { /** * List of all tests that are newly added. */ - readonly added: ReadonlyArray; + readonly added: ReadonlyArray; /** * List of existing tests that have updated. */ - readonly updated: ReadonlyArray; + readonly updated: ReadonlyArray; /** * List of existing tests that have been removed. */ - readonly removed: ReadonlyArray; + readonly removed: ReadonlyArray; /** * Highest node in the test tree under which changes were made. This can * be easily plugged into events like the TreeDataProvider update event. */ - readonly commonChangeAncestor: TestItem | null; + readonly commonChangeAncestor: RequiredTestItem | null; } /** @@ -2094,13 +2177,11 @@ declare module 'vscode' { readonly onDidChangeTest: Event; /** - * An event that should be fired when all tests that are currently defined - * have been discovered. The provider should continue to watch for changes - * and fire `onDidChangeTest` until the hierarchy is disposed. - * - * @todo can this be covered by existing progress apis? Or return a promise + * Promise that should be resolved when all tests that are initially + * defined have been discovered. The provider should continue to watch for + * changes and fire `onDidChangeTest` until the hierarchy is disposed. */ - readonly onDidDiscoverInitialTests: Event; + readonly discoveredInitialTests?: Thenable; /** * Dispose will be called when there are no longer observers interested @@ -2129,20 +2210,23 @@ declare module 'vscode' { * It's guaranteed that this method will not be called again while * there is a previous undisposed watcher for the given workspace folder. */ - createWorkspaceTestHierarchy?(workspace: WorkspaceFolder): TestHierarchy; + // eslint-disable-next-line vscode-dts-provider-naming + createWorkspaceTestHierarchy?(workspace: WorkspaceFolder): TestHierarchy | undefined; /** * Requests that tests be provided for the given document. This will * be called when tests need to be enumerated for a single open file, * for instance by code lens UI. */ - createDocumentTestHierarchy?(document: TextDocument): TestHierarchy; + // eslint-disable-next-line vscode-dts-provider-naming + createDocumentTestHierarchy?(document: TextDocument): TestHierarchy | undefined; /** * Starts a test run. This should cause {@link onDidChangeTest} to * fire with update test states during the run. * @todo this will eventually need to be able to return a summary report, coverage for example. */ + // eslint-disable-next-line vscode-dts-provider-naming runTests?(options: TestRunOptions, cancellationToken: CancellationToken): ProviderResult; } @@ -2172,6 +2256,16 @@ declare module 'vscode' { */ label: string; + /** + * Optional unique identifier for the TestItem. This is used to correlate + * test results and tests in the document with those in the workspace + * (test explorer). This must not change for the lifetime of a test item. + * + * If the ID is not provided, it defaults to the concatenation of the + * item's label and its parent's ID, if any. + */ + readonly id?: string; + /** * Optional description that appears next to the label. */ @@ -2208,19 +2302,30 @@ declare module 'vscode' { state: TestState; } + /** + * A {@link TestItem} with its defaults filled in. + */ + export type RequiredTestItem = { + [K in keyof Required]: K extends 'children' + ? RequiredTestItem[] + : (K extends 'description' | 'location' ? TestItem[K] : Required[K]) + }; + export enum TestRunState { // Initial state Unset = 0, + // Test will be run, but is not currently running. + Queued = 1, // Test is currently running - Running = 1, + Running = 2, // Test run has passed - Passed = 2, + Passed = 3, // Test run has failed (on an assertion) - Failed = 3, + Failed = 4, // Test run has been skipped - Skipped = 4, + Skipped = 5, // Test run failed for some other reason (compilation error, timeout, etc) - Errored = 5 + Errored = 6 } /** @@ -2294,27 +2399,180 @@ declare module 'vscode' { */ location?: Location; } + //#endregion - //#region Statusbar Item Background Color (https://github.com/microsoft/vscode/issues/110214) + //#region Opener service (https://github.com/microsoft/vscode/issues/109277) /** - * A status bar item is a status bar contribution that can - * show text and icons and run a command on click. + * Details if an `ExternalUriOpener` can open a uri. + * + * The priority is also used to rank multiple openers against each other and determine + * if an opener should be selected automatically or if the user should be prompted to + * select an opener. + * + * VS Code will try to use the best available opener, as sorted by `ExternalUriOpenerPriority`. + * If there are multiple potential "best" openers for a URI, then the user will be prompted + * to select an opener. */ - export interface StatusBarItem { + export enum ExternalUriOpenerPriority { + /** + * The opener is disabled and will never be shown to users. + * + * Note that the opener can still be used if the user specifically + * configures it in their settings. + */ + None = 0, /** - * The background color for this entry. - * - * Note: only `new ThemeColor('statusBarItem.errorBackground')` is - * supported for now. More background colors may be supported in the - * future. - * - * Note: when a background color is set, the statusbar may override - * the `color` choice to ensure the entry is readable in all themes. + * The opener can open the uri but will not cause a prompt on its own + * since VS Code always contributes a built-in `Default` opener. */ - backgroundColor: ThemeColor | undefined; + Option = 1, + + /** + * The opener can open the uri. + * + * VS Code's built-in opener has `Default` priority. This means that any additional `Default` + * openers will cause the user to be prompted to select from a list of all potential openers. + */ + Default = 2, + + /** + * The opener can open the uri and should be automatically selected over any + * default openers, include the built-in one from VS Code. + * + * A preferred opener will be automatically selected if no other preferred openers + * are available. If multiple preferred openers are available, then the user + * is shown a prompt with all potential openers (not just preferred openers). + */ + Preferred = 3, + } + + /** + * Handles opening uris to external resources, such as http(s) links. + * + * Extensions can implement an `ExternalUriOpener` to open `http` links to a webserver + * inside of VS Code instead of having the link be opened by the web browser. + * + * Currently openers may only be registered for `http` and `https` uris. + */ + export interface ExternalUriOpener { + + /** + * Check if the opener can open a uri. + * + * @param uri The uri being opened. This is the uri that the user clicked on. It has + * not yet gone through port forwarding. + * @param token Cancellation token indicating that the result is no longer needed. + * + * @return Priority indicating if the opener can open the external uri. + */ + canOpenExternalUri(uri: Uri, token: CancellationToken): ExternalUriOpenerPriority | Thenable; + + /** + * Open a uri. + * + * This is invoked when: + * + * - The user clicks a link which does not have an assigned opener. In this case, first `canOpenExternalUri` + * is called and if the user selects this opener, then `openExternalUri` is called. + * - The user sets the default opener for a link in their settings and then visits a link. + * + * @param resolvedUri The uri to open. This uri may have been transformed by port forwarding, so it + * may not match the original uri passed to `canOpenExternalUri`. Use `ctx.originalUri` to check the + * original uri. + * @param ctx Additional information about the uri being opened. + * @param token Cancellation token indicating that opening has been canceled. + * + * @return Thenable indicating that the opening has completed. + */ + openExternalUri(resolvedUri: Uri, ctx: OpenExternalUriContext, token: CancellationToken): Thenable | void; + } + + /** + * Additional information about the uri being opened. + */ + interface OpenExternalUriContext { + /** + * The uri that triggered the open. + * + * This is the original uri that the user clicked on or that was passed to `openExternal.` + * Due to port forwarding, this may not match the `resolvedUri` passed to `openExternalUri`. + */ + readonly sourceUri: Uri; + } + + /** + * Additional metadata about a registered `ExternalUriOpener`. + */ + interface ExternalUriOpenerMetadata { + + /** + * List of uri schemes the opener is triggered for. + * + * Currently only `http` and `https` are supported. + */ + readonly schemes: readonly string[] + + /** + * Text displayed to the user that explains what the opener does. + * + * For example, 'Open in browser preview' + */ + readonly label: string; + } + + namespace window { + /** + * Register a new `ExternalUriOpener`. + * + * When a uri is about to be opened, an `onUriOpen:SCHEME` activation event is fired. + * + * @param id Unique id of the opener, such as `myExtension.browserPreview`. This is used in settings + * and commands to identify the opener. + * @param opener Opener to register. + * @param metadata Additional information about the opener. + * + * @returns Disposable that unregisters the opener. + */ + export function registerExternalUriOpener(id: string, opener: ExternalUriOpener, metadata: ExternalUriOpenerMetadata): Disposable; + } + + interface OpenExternalOptions { + /** + * Allows using openers contributed by extensions through `registerExternalUriOpener` + * when opening the resource. + * + * If `true`, VS Code will check if any contributed openers can handle the + * uri, and fallback to the default opener behavior. + * + * If it is string, this specifies the id of the `ExternalUriOpener` + * that should be used if it is available. Use `'default'` to force VS Code's + * standard external opener to be used. + */ + readonly allowContributedOpeners?: boolean | string; + } + + namespace env { + export function openExternal(target: Uri, options?: OpenExternalOptions): Thenable; + } + + //endregionn + + //#region https://github.com/Microsoft/vscode/issues/15178 + + // TODO@API must be a class + export interface OpenEditorInfo { + name: string; + resource: Uri; + } + + export namespace window { + export const openEditors: ReadonlyArray; + + // todo@API proper event type + export const onDidChangeOpenEditors: Event; } //#endregion diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 3b4c8a66..1e3169fd 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -17,6 +17,7 @@ import { LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEdito // --- mainThread participants import './mainThreadBulkEdits'; import './mainThreadCodeInsets'; +import './mainThreadCLICommands'; import './mainThreadClipboard'; import './mainThreadCommands'; import './mainThreadConfiguration'; @@ -30,6 +31,7 @@ import './mainThreadDocuments'; import './mainThreadDocumentsAndEditors'; import './mainThreadEditor'; import './mainThreadEditors'; +import './mainThreadEditorTabs'; import './mainThreadErrors'; import './mainThreadExtensionService'; import './mainThreadFileSystem'; @@ -54,6 +56,7 @@ import './mainThreadTheming'; import './mainThreadTreeViews'; import './mainThreadDownloadService'; import './mainThreadUrls'; +import './mainThreadUriOpeners'; import './mainThreadWindow'; import './mainThreadWebviewManager'; import './mainThreadWorkspace'; @@ -65,6 +68,7 @@ import './mainThreadTunnelService'; import './mainThreadAuthentication'; import './mainThreadTimeline'; import './mainThreadTesting'; +import './mainThreadSecretState'; 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 index c3965040..98bb98d8 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -18,9 +18,6 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA import { fromNow } from 'vs/base/common/date'; import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { isWeb } from 'vs/base/common/platform'; -import { IEncryptionService } from 'vs/workbench/services/encryption/common/encryptionService'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials'; const VSO_ALLOWED_EXTENSIONS = ['github.vscode-pull-request-github', 'github.vscode-pull-request-github-insiders', 'vscode.git', 'ms-vsonline.vsonline', 'vscode.github-browser', 'ms-vscode.github-browser', 'github.codespaces']; @@ -225,10 +222,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu @INotificationService private readonly notificationService: INotificationService, @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, @IQuickInputService private readonly quickInputService: IQuickInputService, - @IExtensionService private readonly extensionService: IExtensionService, - @ICredentialsService private readonly credentialsService: ICredentialsService, - @IEncryptionService private readonly encryptionService: IEncryptionService, - @IProductService private readonly productService: IProductService + @IExtensionService private readonly extensionService: IExtensionService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication); @@ -250,10 +244,6 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu this._register(this.authenticationService.onDidChangeDeclaredProviders(e => { this._proxy.$setProviders(e); })); - - this._register(this.credentialsService.onDidChangePassword(_ => { - this._proxy.$onDidChangePassword(); - })); } $getProviderIds(): Promise { @@ -416,7 +406,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu const remoteConnection = this.remoteAgentService.getConnection(); const isVSO = remoteConnection !== null - ? remoteConnection.remoteAuthority.startsWith('vsonline') + ? remoteConnection.remoteAuthority.startsWith('vsonline') || remoteConnection.remoteAuthority.startsWith('codespaces') : isWeb; if (isVSO && VSO_ALLOWED_EXTENSIONS.includes(extensionId)) { @@ -466,46 +456,4 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu this.storageService.store(`${extensionName}-${providerId}`, sessionId, StorageScope.GLOBAL, StorageTarget.MACHINE); addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName); } - - private getFullKey(extensionId: string): string { - return `${this.productService.urlProtocol}${extensionId}`; - } - - async $getPassword(extensionId: string, key: string): Promise { - const fullKey = this.getFullKey(extensionId); - const password = await this.credentialsService.getPassword(fullKey, key); - const decrypted = password && await this.encryptionService.decrypt(password); - - if (decrypted) { - try { - const value = JSON.parse(decrypted); - if (value.extensionId === extensionId) { - return value.content; - } - } catch (_) { - throw new Error('Cannot get password'); - } - } - - return undefined; - } - - async $setPassword(extensionId: string, key: string, value: string): Promise { - const fullKey = this.getFullKey(extensionId); - const toEncrypt = JSON.stringify({ - extensionId, - content: value - }); - const encrypted = await this.encryptionService.encrypt(toEncrypt); - return this.credentialsService.setPassword(fullKey, key, encrypted); - } - - async $deletePassword(extensionId: string, key: string): Promise { - try { - const fullKey = this.getFullKey(extensionId); - await this.credentialsService.deletePassword(fullKey, key); - } catch (_) { - throw new Error('Cannot delete password'); - } - } } diff --git a/src/vs/workbench/api/browser/mainThreadBulkEdits.ts b/src/vs/workbench/api/browser/mainThreadBulkEdits.ts index 70fb045d..8583555b 100644 --- a/src/vs/workbench/api/browser/mainThreadBulkEdits.ts +++ b/src/vs/workbench/api/browser/mainThreadBulkEdits.ts @@ -3,29 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IBulkEditService, ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; -import { IExtHostContext, IWorkspaceEditDto, WorkspaceEditType, MainThreadBulkEditsShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; -import { revive } from 'vs/base/common/marshalling'; -import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; -import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; - -function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] { - if (!data?.edits) { - return []; - } - - const result: ResourceEdit[] = []; - for (let edit of revive(data).edits) { - if (edit._type === WorkspaceEditType.File) { - result.push(new ResourceFileEdit(edit.oldUri, edit.newUri, edit.options, edit.metadata)); - } else if (edit._type === WorkspaceEditType.Text) { - result.push(new ResourceTextEdit(edit.resource, edit.edit, edit.modelVersionId, edit.metadata)); - } else if (edit._type === WorkspaceEditType.Cell) { - result.push(new ResourceNotebookCellEdit(edit.resource, edit.edit, edit.notebookVersionId, edit.metadata)); - } - } - return result; -} +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IExtHostContext, IWorkspaceEditDto, MainThreadBulkEditsShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { reviveWorkspaceEditDto2 } from 'vs/workbench/api/browser/mainThreadEditors'; @extHostNamedCustomer(MainContext.MainThreadBulkEdits) export class MainThreadBulkEdits implements MainThreadBulkEditsShape { @@ -39,12 +19,6 @@ export class MainThreadBulkEdits implements MainThreadBulkEditsShape { $tryApplyWorkspaceEdit(dto: IWorkspaceEditDto, undoRedoGroupId?: number): Promise { const edits = reviveWorkspaceEditDto2(dto); - return this._bulkEditService.apply(edits, { - // having a undoRedoGroupId means that this is a nested workspace edit, - // e.g one from a onWill-handler and for now we need to forcefully suppress - // refactor previewing, see: https://github.com/microsoft/vscode/issues/111873#issuecomment-738739852 - undoRedoGroupId, - suppressPreview: typeof undoRedoGroupId === 'number' ? true : undefined - }).then(() => true, _err => false); + return this._bulkEditService.apply(edits, { undoRedoGroupId }).then(() => true, _err => false); } } diff --git a/src/vs/workbench/api/browser/mainThreadCLICommands.ts b/src/vs/workbench/api/browser/mainThreadCLICommands.ts new file mode 100644 index 00000000..a02cdf6e --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadCLICommands.ts @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Schemas } from 'vs/base/common/network'; +import { isString } from 'vs/base/common/types'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { CLIOutput, IExtensionGalleryService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService'; +import { getExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { canExecuteOnWorkspace } from 'vs/workbench/services/extensions/common/extensionsUtil'; +import { IExtensionManifest } from 'vs/workbench/workbench.web.api'; + + +// this class contains the commands that the CLI server is reying on + +CommandsRegistry.registerCommand('_remoteCLI.openExternal', function (accessor: ServicesAccessor, uri: UriComponents, options: { allowTunneling?: boolean }) { + // TODO: discuss martin, ben where to put this + const openerService = accessor.get(IOpenerService); + openerService.open(URI.revive(uri), { openExternal: true, allowTunneling: options?.allowTunneling === true }); +}); + +interface ManageExtensionsArgs { + list?: { showVersions?: boolean, category?: string; }; + install?: (string | URI)[]; + uninstall?: string[]; + force?: boolean; +} + +CommandsRegistry.registerCommand('_remoteCLI.manageExtensions', async function (accessor: ServicesAccessor, args: ManageExtensionsArgs) { + + const instantiationService = accessor.get(IInstantiationService); + const extensionManagementServerService = accessor.get(IExtensionManagementServerService); + const remoteExtensionManagementService = extensionManagementServerService.remoteExtensionManagementServer?.extensionManagementService; + if (!remoteExtensionManagementService) { + return; + } + + const cliService = instantiationService.createChild(new ServiceCollection([IExtensionManagementService, remoteExtensionManagementService])).createInstance(RemoteExtensionCLIManagementService); + + const lines: string[] = []; + const output = { log: lines.push.bind(lines), error: lines.push.bind(lines) }; + + if (args.list) { + await cliService.listExtensions(!!args.list.showVersions, args.list.category, output); + } else { + const revive = (inputs: (string | UriComponents)[]) => inputs.map(input => isString(input) ? input : URI.revive(input)); + if (Array.isArray(args.install) && args.install.length) { + try { + await cliService.installExtensions(revive(args.install), [], true, !!args.force, output); + } catch (e) { + lines.push(e.message); + } + } + if (Array.isArray(args.uninstall) && args.uninstall.length) { + try { + await cliService.uninstallExtensions(revive(args.uninstall), !!args.force, output); + } catch (e) { + lines.push(e.message); + } + } + } + return lines.join('\n'); +}); + +class RemoteExtensionCLIManagementService extends ExtensionManagementCLIService { + + private _location: string | undefined; + + constructor( + @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IProductService private readonly productService: IProductService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, + @ILocalizationsService localizationsService: ILocalizationsService, + @ILabelService labelService: ILabelService, + @IWorkbenchEnvironmentService envService: IWorkbenchEnvironmentService + ) { + super(extensionManagementService, extensionGalleryService, localizationsService); + + const remoteAuthority = envService.remoteAuthority; + this._location = remoteAuthority ? labelService.getHostLabel(Schemas.vscodeRemote, remoteAuthority) : undefined; + } + + protected get location(): string | undefined { + return this._location; + } + + protected validateExtensionKind(manifest: IExtensionManifest, output: CLIOutput): boolean { + if (!canExecuteOnWorkspace(manifest, this.productService, this.configurationService)) { + output.log(localize('cannot be installed', "Cannot install '{0}' because this extension has defined that it cannot run on the remote server.", getExtensionId(manifest.publisher, manifest.name))); + return false; + } + return true; + } +} diff --git a/src/vs/workbench/api/browser/mainThreadConsole.ts b/src/vs/workbench/api/browser/mainThreadConsole.ts index 4a244875..d678f21b 100644 --- a/src/vs/workbench/api/browser/mainThreadConsole.ts +++ b/src/vs/workbench/api/browser/mainThreadConsole.ts @@ -10,22 +10,18 @@ import { IRemoteConsoleLog, log } from 'vs/base/common/console'; import { logRemoteEntry } from 'vs/workbench/services/extensions/common/remoteConsoleUtil'; import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions'; import { ILogService } from 'vs/platform/log/common/log'; -import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; @extHostNamedCustomer(MainContext.MainThreadConsole) export class MainThreadConsole implements MainThreadConsoleShape { - private readonly _isExtensionDevHost: boolean; private readonly _isExtensionDevTestFromCli: boolean; constructor( - extHostContext: IExtHostContext, + _extHostContext: IExtHostContext, @IEnvironmentService private readonly _environmentService: IEnvironmentService, @ILogService private readonly _logService: ILogService, - @IExtensionHostDebugService private readonly _extensionHostDebugService: IExtensionHostDebugService, ) { const devOpts = parseExtensionDevOptions(this._environmentService); - this._isExtensionDevHost = devOpts.isExtensionDevHost; this._isExtensionDevTestFromCli = devOpts.isExtensionDevTestFromCli; } @@ -43,10 +39,5 @@ export class MainThreadConsole implements MainThreadConsoleShape { if (this._isExtensionDevTestFromCli) { logRemoteEntry(this._logService, entry); } - - // Broadcast to other windows if we are in development mode - else if (this._environmentService.debugExtensionHost.debugId && (!this._environmentService.isBuilt || this._isExtensionDevHost)) { - this._extensionHostDebugService.logToSession(this._environmentService.debugExtensionHost.debugId, entry); - } } } diff --git a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts index bcf31a1a..e5f1d3f5 100644 --- a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { multibyteAwareBtoa } from 'vs/base/browser/dom'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, DisposableStore, IDisposable, IReference } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, dispose, IDisposable, IReference } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { basename } from 'vs/base/common/path'; import { isEqual, isEqualOrParent, toLocalResource } from 'vs/base/common/resources'; -import { multibyteAwareBtoa } from 'vs/base/browser/dom'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as modes from 'vs/editor/common/modes'; import { localize } from 'vs/nls'; @@ -95,10 +95,7 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc dispose() { super.dispose(); - for (const disposable of this._editorProviders.values()) { - disposable.dispose(); - } - + dispose(this._editorProviders.values()); this._editorProviders.clear(); } diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index f1906f16..cb4c3985 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -327,7 +327,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return { id: sessionID, type: session.configuration.type, - name: session.configuration.name, + name: session.name, folderUri: session.root ? session.root.uri : undefined, configuration: session.configuration }; diff --git a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts new file mode 100644 index 00000000..2e6aaffa --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { ExtHostContext, IExtHostEditorTabsShape, IExtHostContext, MainContext, IEditorTabDto } from 'vs/workbench/api/common/extHost.protocol'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { Verbosity } from 'vs/workbench/common/editor'; +import { GroupChangeKind, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; + +export interface ITabInfo { + name: string; + resource: URI; +} + +@extHostNamedCustomer(MainContext.MainThreadEditorTabs) +export class MainThreadEditorTabs { + + private static _GroupEventFilter = new Set([GroupChangeKind.EDITOR_CLOSE, GroupChangeKind.EDITOR_OPEN]); + + private readonly _dispoables = new DisposableStore(); + private readonly _groups = new Map(); + private readonly _proxy: IExtHostEditorTabsShape; + + constructor( + extHostContext: IExtHostContext, + @IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService, + ) { + + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditorTabs); + + this._editorGroupsService.groups.forEach(this._subscribeToGroup, this); + this._dispoables.add(_editorGroupsService.onDidAddGroup(this._subscribeToGroup, this)); + this._dispoables.add(_editorGroupsService.onDidRemoveGroup(e => { + const subscription = this._groups.get(e); + if (subscription) { + subscription.dispose(); + this._groups.delete(e); + this._pushEditorTabs(); + } + })); + this._pushEditorTabs(); + } + + dispose(): void { + dispose(this._groups.values()); + this._dispoables.dispose(); + } + + private _subscribeToGroup(group: IEditorGroup) { + this._groups.get(group)?.dispose(); + const listener = group.onDidGroupChange(e => { + if (MainThreadEditorTabs._GroupEventFilter.has(e.kind)) { + this._pushEditorTabs(); + } + }); + this._groups.set(group, listener); + } + + private _pushEditorTabs(): void { + const tabs: IEditorTabDto[] = []; + for (const group of this._editorGroupsService.groups) { + for (const editor of group.editors) { + if (editor.isDisposed() || !editor.resource) { + continue; + } + tabs.push({ + group: group.id, + name: editor.getTitle(Verbosity.SHORT) ?? '', + resource: editor.resource + }); + } + } + + this._proxy.$acceptEditorTabs(tabs); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index 1723f632..0f52d9af 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -27,7 +27,7 @@ import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/wo import { revive } from 'vs/base/common/marshalling'; import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; -function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] { +export function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] { if (!data?.edits) { return []; } diff --git a/src/vs/workbench/api/browser/mainThreadExtensionService.ts b/src/vs/workbench/api/browser/mainThreadExtensionService.ts index 5d0132e2..11cfcc3d 100644 --- a/src/vs/workbench/api/browser/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadExtensionService.ts @@ -7,7 +7,7 @@ import { SerializedError } from 'vs/base/common/errors'; import Severity from 'vs/base/common/severity'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { IExtHostContext, MainContext, MainThreadExtensionServiceShape } from 'vs/workbench/api/common/extHost.protocol'; -import { IExtensionService, ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionService, ExtensionActivationError, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { localize } from 'vs/nls'; @@ -19,29 +19,23 @@ import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/com import { CancellationToken } from 'vs/base/common/cancellation'; import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; +import { ITimerService } from 'vs/workbench/services/timer/browser/timerService'; @extHostNamedCustomer(MainContext.MainThreadExtensionService) export class MainThreadExtensionService implements MainThreadExtensionServiceShape { - private readonly _extensionService: IExtensionService; - private readonly _notificationService: INotificationService; - private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService; - private readonly _hostService: IHostService; - private readonly _extensionEnablementService: IWorkbenchExtensionEnablementService; + private readonly _extensionHostKind: ExtensionHostKind; constructor( extHostContext: IExtHostContext, - @IExtensionService extensionService: IExtensionService, - @INotificationService notificationService: INotificationService, - @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, - @IHostService hostService: IHostService, - @IWorkbenchExtensionEnablementService extensionEnablementService: IWorkbenchExtensionEnablementService + @IExtensionService private readonly _extensionService: IExtensionService, + @INotificationService private readonly _notificationService: INotificationService, + @IExtensionsWorkbenchService private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService, + @IHostService private readonly _hostService: IHostService, + @IWorkbenchExtensionEnablementService private readonly _extensionEnablementService: IWorkbenchExtensionEnablementService, + @ITimerService private readonly _timerService: ITimerService, ) { - this._extensionService = extensionService; - this._notificationService = notificationService; - this._extensionsWorkbenchService = extensionsWorkbenchService; - this._hostService = hostService; - this._extensionEnablementService = extensionEnablementService; + this._extensionHostKind = extHostContext.extensionHostKind; } public dispose(): void { @@ -131,4 +125,14 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha async $onExtensionHostExit(code: number): Promise { this._extensionService._onExtensionHostExit(code); } + + async $setPerformanceMarks(marks: PerformanceMark[]): Promise { + if (this._extensionHostKind === ExtensionHostKind.LocalProcess) { + this._timerService.setPerformanceMarks('localExtHost', marks); + } else if (this._extensionHostKind === ExtensionHostKind.LocalWebWorker) { + this._timerService.setPerformanceMarks('workerExtHost', marks); + } else { + this._timerService.setPerformanceMarks('remoteExtHost', marks); + } + } } diff --git a/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts b/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts index 81905c6b..c0394333 100644 --- a/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts +++ b/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts @@ -4,23 +4,43 @@ *--------------------------------------------------------------------------------------------*/ import { DisposableStore } from 'vs/base/common/lifecycle'; -import { FileChangeType, IFileService } from 'vs/platform/files/common/files'; +import { FileChangeType, FileOperation, IFileService } from 'vs/platform/files/common/files'; import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ExtHostContext, FileSystemEvents, IExtHostContext } from '../common/extHost.protocol'; import { localize } from 'vs/nls'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { IWorkingCopyFileOperationParticipant, IWorkingCopyFileService, SourceTargetPair, IFileOperationUndoRedoInfo } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { reviveWorkspaceEditDto2 } from 'vs/workbench/api/browser/mainThreadEditors'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { raceCancellation } from 'vs/base/common/async'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import Severity from 'vs/base/common/severity'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @extHostCustomer export class MainThreadFileSystemEventService { + static readonly MementoKeyAdditionalEdits = `file.particpants.additionalEdits`; + private readonly _listener = new DisposableStore(); constructor( extHostContext: IExtHostContext, @IFileService fileService: IFileService, - @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService + @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, + @IBulkEditService bulkEditService: IBulkEditService, + @IProgressService progressService: IProgressService, + @IDialogService dialogService: IDialogService, + @IStorageService storageService: IStorageService, + @ILogService logService: ILogService, + @IEnvironmentService envService: IEnvironmentService ) { const proxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystemEventService); @@ -53,14 +73,124 @@ export class MainThreadFileSystemEventService { })); - // BEFORE file operation - this._listener.add(workingCopyFileService.addFileOperationParticipant({ - participate: async (files, operation, undoRedoGroupId, isUndoing, _progress, timeout, token) => { - if (!isUndoing) { - return proxy.$onWillRunFileOperation(operation, files, undoRedoGroupId, timeout, token); + const fileOperationParticipant = new class implements IWorkingCopyFileOperationParticipant { + async participate(files: SourceTargetPair[], operation: FileOperation, undoInfo: IFileOperationUndoRedoInfo | undefined, timeout: number, token: CancellationToken) { + if (undoInfo?.isUndoing) { + return; + } + + const cts = new CancellationTokenSource(token); + const timer = setTimeout(() => cts.cancel(), timeout); + + const data = await progressService.withProgress({ + location: ProgressLocation.Notification, + title: this._progressLabel(operation), + cancellable: true, + delay: Math.min(timeout / 2, 3000) + }, () => { + // race extension host event delivery against timeout AND user-cancel + const onWillEvent = proxy.$onWillRunFileOperation(operation, files, timeout, token); + return raceCancellation(onWillEvent, cts.token); + }, () => { + // user-cancel + cts.cancel(); + + }).finally(() => { + cts.dispose(); + clearTimeout(timer); + }); + + if (!data) { + // cancelled or no reply + return; + } + + const needsConfirmation = data.edit.edits.some(edit => edit.metadata?.needsConfirmation); + let showPreview = storageService.getBoolean(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, StorageScope.GLOBAL); + + if (envService.extensionTestsLocationURI) { + // don't show dialog in tests + showPreview = false; + } + + if (showPreview === undefined) { + // show a user facing message + + let message: string; + if (data.extensionNames.length === 1) { + if (operation === FileOperation.CREATE) { + message = localize('ask.1.create', "Extension '{0}' wants to make refactoring changes with this file creation", data.extensionNames[0]); + } else if (operation === FileOperation.COPY) { + message = localize('ask.1.copy', "Extension '{0}' wants to make refactoring changes with this file copy", data.extensionNames[0]); + } else if (operation === FileOperation.MOVE) { + message = localize('ask.1.move', "Extension '{0}' wants to make refactoring changes with this file move", data.extensionNames[0]); + } else /* if (operation === FileOperation.DELETE) */ { + message = localize('ask.1.delete', "Extension '{0}' wants to make refactoring changes with this file deletion", data.extensionNames[0]); + } + } else { + if (operation === FileOperation.CREATE) { + message = localize('ask.N.create', "{0} extensions want to make refactoring changes with this file creation", data.extensionNames.length); + } else if (operation === FileOperation.COPY) { + message = localize('ask.N.copy', "{0} extensions want to make refactoring changes with this file copy", data.extensionNames.length); + } else if (operation === FileOperation.MOVE) { + message = localize('ask.N.move', "{0} extensions want to make refactoring changes with this file move", data.extensionNames.length); + } else /* if (operation === FileOperation.DELETE) */ { + message = localize('ask.N.delete', "{0} extensions want to make refactoring changes with this file deletion", data.extensionNames.length); + } + } + + if (needsConfirmation) { + // edit which needs confirmation -> always show dialog + const answer = await dialogService.show(Severity.Info, message, [localize('preview', "Show Preview"), localize('cancel', "Skip Changes")], { cancelId: 1 }); + showPreview = true; + if (answer.choice === 1) { + // no changes wanted + return; + } + } else { + // choice + const answer = await dialogService.show(Severity.Info, message, + [localize('ok', "OK"), localize('preview', "Show Preview"), localize('cancel', "Skip Changes")], + { + cancelId: 2, + checkbox: { label: localize('again', "Don't ask again") } + } + ); + if (answer.choice === 2) { + // no changes wanted, don't persist cancel option + return; + } + showPreview = answer.choice === 1; + if (answer.checkboxChecked /* && answer.choice !== 2 */) { + storageService.store(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, showPreview, StorageScope.GLOBAL, StorageTarget.USER); + } + } + } + + logService.info('[onWill-handler] applying additional workspace edit from extensions', data.extensionNames); + + await bulkEditService.apply( + reviveWorkspaceEditDto2(data.edit), + { undoRedoGroupId: undoInfo?.undoRedoGroupId, showPreview } + ); + } + + private _progressLabel(operation: FileOperation): string { + switch (operation) { + case FileOperation.CREATE: + return localize('msg-create', "Running 'File Create' participants..."); + case FileOperation.MOVE: + return localize('msg-rename', "Running 'File Rename' participants..."); + case FileOperation.COPY: + return localize('msg-copy', "Running 'File Copy' participants..."); + case FileOperation.DELETE: + return localize('msg-delete', "Running 'File Delete' participants..."); } } - })); + }; + + // BEFORE file operation + this._listener.add(workingCopyFileService.addFileOperationParticipant(fileOperationParticipant)); // AFTER file operation this._listener.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => proxy.$onDidRunFileOperation(e.operation, e.files))); @@ -71,6 +201,19 @@ export class MainThreadFileSystemEventService { } } +registerAction2(class ResetMemento extends Action2 { + constructor() { + super({ + id: 'files.participants.resetChoice', + title: localize('label', "Reset choice for 'File operation needs preview'"), + f1: true + }); + } + run(accessor: ServicesAccessor) { + accessor.get(IStorageService).remove(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, StorageScope.GLOBAL); + } +}); + Registry.as(Extensions.Configuration).registerConfiguration({ id: 'files', diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index ddea8e0e..391cc7ad 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -21,7 +21,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import * as callh from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { mixin } from 'vs/base/common/objects'; -import { decodeSemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokensDto'; +import { decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesShape { @@ -497,6 +497,32 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha })); } + // --- inline hints + + $registerInlineHintsProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void { + const provider = { + provideInlineHints: async (model: ITextModel, range: EditorRange, token: CancellationToken): Promise => { + const result = await this._proxy.$provideInlineHints(handle, model.uri, range, token); + return result?.hints; + } + }; + + if (typeof eventHandle === 'number') { + const emitter = new Emitter(); + this._registrations.set(eventHandle, emitter); + provider.onDidChangeInlineHints = emitter.event; + } + + this._registrations.set(handle, modes.InlineHintsProviderRegistry.register(selector, provider)); + } + + $emitInlineHintsEvent(eventHandle: number, event?: any): void { + const obj = this._registrations.get(eventHandle); + if (obj instanceof Emitter) { + obj.fire(event); + } + } + // --- links $registerDocumentLinkProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean): void { @@ -662,7 +688,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha return { beforeText: MainThreadLanguageFeatures._reviveRegExp(onEnterRule.beforeText), afterText: onEnterRule.afterText ? MainThreadLanguageFeatures._reviveRegExp(onEnterRule.afterText) : undefined, - oneLineAboveText: onEnterRule.oneLineAboveText ? MainThreadLanguageFeatures._reviveRegExp(onEnterRule.oneLineAboveText) : undefined, + previousLineText: onEnterRule.previousLineText ? MainThreadLanguageFeatures._reviveRegExp(onEnterRule.previousLineText) : undefined, action: onEnterRule.action }; } diff --git a/src/vs/workbench/api/browser/mainThreadMessageService.ts b/src/vs/workbench/api/browser/mainThreadMessageService.ts index c956da47..b7470ac8 100644 --- a/src/vs/workbench/api/browser/mainThreadMessageService.ts +++ b/src/vs/workbench/api/browser/mainThreadMessageService.ts @@ -33,7 +33,7 @@ export class MainThreadMessageService implements MainThreadMessageServiceShape { $showMessage(severity: Severity, message: string, options: MainThreadMessageOptions, commands: { title: string; isCloseAffordance: boolean; handle: number; }[]): Promise { if (options.modal) { - return this._showModalMessage(severity, message, commands); + return this._showModalMessage(severity, message, commands, options.useCustom); } else { return this._showMessage(severity, message, commands, options.extension); } @@ -97,7 +97,7 @@ export class MainThreadMessageService implements MainThreadMessageServiceShape { }); } - private async _showModalMessage(severity: Severity, message: string, commands: { title: string; isCloseAffordance: boolean; handle: number; }[]): Promise { + private async _showModalMessage(severity: Severity, message: string, commands: { title: string; isCloseAffordance: boolean; handle: number; }[], useCustom?: boolean): Promise { let cancelId: number | undefined = undefined; const buttons = commands.map((command, index) => { @@ -118,7 +118,7 @@ export class MainThreadMessageService implements MainThreadMessageServiceShape { cancelId = buttons.length - 1; } - const { choice } = await this._dialogService.show(severity, message, buttons, { cancelId }); + const { choice } = await this._dialogService.show(severity, message, buttons, { cancelId, useCustom }); return choice === commands.length ? undefined : commands[choice].handle; } } diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index cbf34422..0b4b7f3f 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -16,8 +16,8 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { EditorActivation, ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { viewColumnToEditorGroup } from 'vs/workbench/common/editor'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -129,12 +129,12 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, - @IQuickInputService private readonly quickInputService: IQuickInputService, @ILogService private readonly logService: ILogService, @INotebookCellStatusBarService private readonly cellStatusBarService: INotebookCellStatusBarService, @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, @INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService, @IUriIdentityService private readonly _uriIdentityService: IUriIdentityService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebook); @@ -646,14 +646,13 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo switch (revealType) { case NotebookEditorRevealType.Default: - notebookEditor.revealInView(cell); - break; + return notebookEditor.revealCellRangeInView(range); case NotebookEditorRevealType.InCenter: - notebookEditor.revealInCenter(cell); - break; + return notebookEditor.revealInCenter(cell); case NotebookEditorRevealType.InCenterIfOutsideViewport: - notebookEditor.revealInCenterIfOutsideViewport(cell); - break; + return notebookEditor.revealInCenterIfOutsideViewport(cell); + case NotebookEditorRevealType.AtTop: + return notebookEditor.revealInViewAtTop(cell); default: break; } @@ -733,7 +732,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo const input = this.editorService.createEditorInput({ resource: URI.revive(resource), options: editorOptions }); // TODO: handle options.selection - const editorPane = await openEditorWith(input, viewType, options, group, this.editorService, this.configurationService, this.quickInputService); + const editorPane = await this._instantiationService.invokeFunction(openEditorWith, input, viewType, options, group); const notebookEditor = (editorPane as unknown as { isNotebookEditor?: boolean })?.isNotebookEditor ? (editorPane!.getControl() as INotebookEditor) : undefined; if (notebookEditor) { diff --git a/src/vs/workbench/api/browser/mainThreadSecretState.ts b/src/vs/workbench/api/browser/mainThreadSecretState.ts new file mode 100644 index 00000000..4067aced --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadSecretState.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials'; +import { IEncryptionService } from 'vs/workbench/services/encryption/common/encryptionService'; +import { ExtHostContext, ExtHostSecretStateShape, IExtHostContext, MainContext, MainThreadSecretStateShape } from '../common/extHost.protocol'; + +@extHostNamedCustomer(MainContext.MainThreadSecretState) +export class MainThreadSecretState extends Disposable implements MainThreadSecretStateShape { + private readonly _proxy: ExtHostSecretStateShape; + + constructor( + extHostContext: IExtHostContext, + @ICredentialsService private readonly credentialsService: ICredentialsService, + @IEncryptionService private readonly encryptionService: IEncryptionService, + @IProductService private readonly productService: IProductService + ) { + super(); + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostSecretState); + + this._register(this.credentialsService.onDidChangePassword(e => { + const extensionId = e.service.substring(this.productService.urlProtocol.length); + this._proxy.$onDidChangePassword({ extensionId, key: e.account }); + })); + } + + private getFullKey(extensionId: string): string { + return `${this.productService.urlProtocol}${extensionId}`; + } + + async $getPassword(extensionId: string, key: string): Promise { + const fullKey = this.getFullKey(extensionId); + const password = await this.credentialsService.getPassword(fullKey, key); + const decrypted = password && await this.encryptionService.decrypt(password); + + if (decrypted) { + try { + const value = JSON.parse(decrypted); + if (value.extensionId === extensionId) { + return value.content; + } + } catch (_) { + throw new Error('Cannot get password'); + } + } + + return undefined; + } + + async $setPassword(extensionId: string, key: string, value: string): Promise { + const fullKey = this.getFullKey(extensionId); + const toEncrypt = JSON.stringify({ + extensionId, + content: value + }); + const encrypted = await this.encryptionService.encrypt(toEncrypt); + return this.credentialsService.setPassword(fullKey, key, encrypted); + } + + async $deletePassword(extensionId: string, key: string): Promise { + try { + const fullKey = this.getFullKey(extensionId); + await this.credentialsService.deletePassword(fullKey, key); + } catch (_) { + throw new Error('Cannot delete password'); + } + } +} diff --git a/src/vs/workbench/api/browser/mainThreadTask.ts b/src/vs/workbench/api/browser/mainThreadTask.ts index 2f4cb073..3cc31f08 100644 --- a/src/vs/workbench/api/browser/mainThreadTask.ts +++ b/src/vs/workbench/api/browser/mainThreadTask.ts @@ -567,13 +567,19 @@ export class MainThreadTask implements MainThreadTaskShape { if (!task) { reject(new Error('Task not found')); } else { - this._taskService.run(task).then(undefined, reason => { - // eat the error, it has already been surfaced to the user and we don't care about it here - }); const result: TaskExecutionDTO = { id: value.id, task: TaskDTO.from(task) }; + this._taskService.run(task).then(summary => { + // Ensure that the task execution gets cleaned up if the exit code is undefined + // This can happen when the task has dependent tasks and one of them failed + if ((summary?.exitCode === undefined) || (summary.exitCode !== 0)) { + this._proxy.$OnDidEndTask(result); + } + }, reason => { + // eat the error, it has already been surfaced to the user and we don't care about it here + }); resolve(result); } }, (_error) => { diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 8e54cb8f..8dad46c6 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { DisposableStore, Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { IShellLaunchConfig, ITerminalProcessExtHostProxy, ISpawnExtHostProcessRequest, ITerminalDimensions, EXT_HOST_CREATION_DELAY, IAvailableShellsRequest, IDefaultShellAndArgsRequest, IStartExtensionTerminalRequest } from 'vs/workbench/contrib/terminal/common/terminal'; -import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, IShellLaunchConfigDto, TerminalLaunchConfig, ITerminalDimensionsDto } from 'vs/workbench/api/common/extHost.protocol'; +import { IShellLaunchConfig, ITerminalProcessExtHostProxy, ISpawnExtHostProcessRequest, ITerminalDimensions, IAvailableShellsRequest, IDefaultShellAndArgsRequest, IStartExtensionTerminalRequest, ITerminalConfiguration, TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, IShellLaunchConfigDto, TerminalLaunchConfig, ITerminalDimensionsDto, TerminalIdentifier } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { URI } from 'vs/base/common/uri'; import { StopWatch } from 'vs/base/common/stopwatch'; @@ -16,11 +16,18 @@ import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/termi import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { deserializeEnvironmentVariableCollection, serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; import { ILogService } from 'vs/platform/log/common/log'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @extHostNamedCustomer(MainContext.MainThreadTerminalService) export class MainThreadTerminalService implements MainThreadTerminalServiceShape { private _proxy: ExtHostTerminalServiceShape; + /** + * Stores a map from a temporary terminal id (a UUID generated on the extension host side) + * to a numeric terminal id (an id generated on the renderer side) + * This comes in play only when dealing with terminals created on the extension host side + */ + private _extHostTerminalIds = new Map(); private _remoteAuthority: string | null; private readonly _toDispose = new DisposableStore(); private readonly _terminalProcessProxies = new Map(); @@ -40,6 +47,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IEnvironmentVariableService private readonly _environmentVariableService: IEnvironmentVariableService, + @IConfigurationService private readonly _configurationService: IConfigurationService, @ILogService private readonly _logService: ILogService, ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTerminalService); @@ -47,13 +55,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape // ITerminalService listeners this._toDispose.add(_terminalService.onInstanceCreated((instance) => { - // Delay this message so the TerminalInstance constructor has a chance to finish and - // return the ID normally to the extension host. The ID that is passed here will be - // used to register non-extension API terminals in the extension host. - setTimeout(() => { - this._onTerminalOpened(instance); - this._onInstanceDimensionsChanged(instance); - }, EXT_HOST_CREATION_DELAY); + this._onTerminalOpened(instance); + this._onInstanceDimensionsChanged(instance); })); this._toDispose.add(_terminalService.onInstanceDisposed(instance => this._onTerminalDisposed(instance))); @@ -100,7 +103,22 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape // when the extension host process goes down ? } - public $createTerminal(launchConfig: TerminalLaunchConfig): Promise<{ id: number, name: string }> { + private _getTerminalId(id: TerminalIdentifier): number | undefined { + if (typeof id === 'number') { + return id; + } + return this._extHostTerminalIds.get(id); + } + + private _getTerminalInstance(id: TerminalIdentifier): ITerminalInstance | undefined { + const rendererId = this._getTerminalId(id); + if (typeof rendererId === 'number') { + return this._terminalService.getInstanceFromId(rendererId); + } + return undefined; + } + + public async $createTerminal(extHostTerminalId: string, launchConfig: TerminalLaunchConfig): Promise { const shellLaunchConfig: IShellLaunchConfig = { name: launchConfig.name, executable: launchConfig.shellPath, @@ -112,39 +130,38 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape strictEnv: launchConfig.strictEnv, hideFromUser: launchConfig.hideFromUser, isExtensionTerminal: launchConfig.isExtensionTerminal, + extHostTerminalId: extHostTerminalId, isFeatureTerminal: launchConfig.isFeatureTerminal }; const terminal = this._terminalService.createTerminal(shellLaunchConfig); - return Promise.resolve({ - id: terminal.id, - name: terminal.title - }); + this._extHostTerminalIds.set(extHostTerminalId, terminal.id); } - public $show(terminalId: number, preserveFocus: boolean): void { - const terminalInstance = this._terminalService.getInstanceFromId(terminalId); + public $show(id: TerminalIdentifier, preserveFocus: boolean): void { + const terminalInstance = this._getTerminalInstance(id); if (terminalInstance) { this._terminalService.setActiveInstance(terminalInstance); this._terminalService.showPanel(!preserveFocus); } } - public $hide(terminalId: number): void { + public $hide(id: TerminalIdentifier): void { + const rendererId = this._getTerminalId(id); const instance = this._terminalService.getActiveInstance(); - if (instance && instance.id === terminalId) { + if (instance && instance.id === rendererId) { this._terminalService.hidePanel(); } } - public $dispose(terminalId: number): void { - const terminalInstance = this._terminalService.getInstanceFromId(terminalId); + public $dispose(id: TerminalIdentifier): void { + const terminalInstance = this._getTerminalInstance(id); if (terminalInstance) { terminalInstance.dispose(); } } - public $sendText(terminalId: number, text: string, addNewLine: boolean): void { - const terminalInstance = this._terminalService.getInstanceFromId(terminalId); + public $sendText(id: TerminalIdentifier, text: string, addNewLine: boolean): void { + const terminalInstance = this._getTerminalInstance(id); if (terminalInstance) { terminalInstance.sendText(text, addNewLine); } @@ -204,6 +221,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape } private _onTerminalOpened(terminalInstance: ITerminalInstance): void { + const extHostTerminalId = terminalInstance.shellLaunchConfig.extHostTerminalId; const shellLaunchConfigDto: IShellLaunchConfigDto = { name: terminalInstance.shellLaunchConfig.name, executable: terminalInstance.shellLaunchConfig.executable, @@ -212,13 +230,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape env: terminalInstance.shellLaunchConfig.env, hideFromUser: terminalInstance.shellLaunchConfig.hideFromUser }; - if (terminalInstance.title) { - this._proxy.$acceptTerminalOpened(terminalInstance.id, terminalInstance.title, shellLaunchConfigDto); - } else { - terminalInstance.waitForTitle().then(title => { - this._proxy.$acceptTerminalOpened(terminalInstance.id, title, shellLaunchConfigDto); - }); - } + this._proxy.$acceptTerminalOpened(terminalInstance.id, extHostTerminalId, terminalInstance.title, shellLaunchConfigDto); } private _onTerminalProcessIdReady(terminalInstance: ITerminalInstance): void { @@ -249,7 +261,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape executable: request.shellLaunchConfig.executable, args: request.shellLaunchConfig.args, cwd: request.shellLaunchConfig.cwd, - env: request.shellLaunchConfig.env + env: request.shellLaunchConfig.env, + flowControl: this._configurationService.getValue(TERMINAL_CONFIG_SECTION).flowControl }; this._logService.trace('Spawning ext host process', { terminalId: proxy.terminalId, shellLaunchConfigDto, request }); @@ -260,8 +273,9 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape request.cols, request.rows, request.isWorkspaceShellAllowed - ).then(request.callback); + ).then(request.callback, request.callback); + proxy.onAcknowledgeDataEvent(charCount => this._proxy.$acceptProcessAckDataEvent(proxy.terminalId, charCount)); proxy.onInput(data => this._proxy.$acceptProcessInput(proxy.terminalId, data)); proxy.onResize(dimensions => this._proxy.$acceptProcessResize(proxy.terminalId, dimensions.cols, dimensions.rows)); proxy.onShutdown(immediate => this._proxy.$acceptProcessShutdown(proxy.terminalId, immediate)); @@ -294,38 +308,59 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape } public $sendProcessTitle(terminalId: number, title: string): void { - this._getTerminalProcess(terminalId).emitTitle(title); + const terminalProcess = this._terminalProcessProxies.get(terminalId); + if (terminalProcess) { + terminalProcess.emitTitle(title); + } } public $sendProcessData(terminalId: number, data: string): void { - this._getTerminalProcess(terminalId).emitData(data); + const terminalProcess = this._terminalProcessProxies.get(terminalId); + if (terminalProcess) { + terminalProcess.emitData(data); + } } public $sendProcessReady(terminalId: number, pid: number, cwd: string): void { - this._getTerminalProcess(terminalId).emitReady(pid, cwd); + const terminalProcess = this._terminalProcessProxies.get(terminalId); + if (terminalProcess) { + terminalProcess.emitReady(pid, cwd); + } } public $sendProcessExit(terminalId: number, exitCode: number | undefined): void { - this._getTerminalProcess(terminalId).emitExit(exitCode); - this._terminalProcessProxies.delete(terminalId); + const terminalProcess = this._terminalProcessProxies.get(terminalId); + if (terminalProcess) { + terminalProcess.emitExit(exitCode); + this._terminalProcessProxies.delete(terminalId); + } } public $sendOverrideDimensions(terminalId: number, dimensions: ITerminalDimensions | undefined): void { - this._getTerminalProcess(terminalId).emitOverrideDimensions(dimensions); + const terminalProcess = this._terminalProcessProxies.get(terminalId); + if (terminalProcess) { + terminalProcess.emitOverrideDimensions(dimensions); + } } public $sendProcessInitialCwd(terminalId: number, initialCwd: string): void { - this._getTerminalProcess(terminalId).emitInitialCwd(initialCwd); + const terminalProcess = this._terminalProcessProxies.get(terminalId); + if (terminalProcess) { + terminalProcess.emitInitialCwd(initialCwd); + } } public $sendProcessCwd(terminalId: number, cwd: string): void { - this._getTerminalProcess(terminalId).emitCwd(cwd); + const terminalProcess = this._terminalProcessProxies.get(terminalId); + if (terminalProcess) { + terminalProcess.emitCwd(cwd); + } } public $sendResolvedLaunchConfig(terminalId: number, shellLaunchConfig: IShellLaunchConfig): void { const instance = this._terminalService.getInstanceFromId(terminalId); if (instance) { - this._getTerminalProcess(terminalId).emitResolvedShellLaunchConfig(shellLaunchConfig); + this._getTerminalProcess(terminalId)?.emitResolvedShellLaunchConfig(shellLaunchConfig); } } @@ -338,7 +373,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape sw.stop(); sum += sw.elapsed(); } - this._getTerminalProcess(terminalId).emitLatency(sum / COUNT); + this._getTerminalProcess(terminalId)?.emitLatency(sum / COUNT); } private _isPrimaryExtHost(): boolean { @@ -363,10 +398,11 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape } } - private _getTerminalProcess(terminalId: number): ITerminalProcessExtHostProxy { + private _getTerminalProcess(terminalId: number): ITerminalProcessExtHostProxy | undefined { const terminal = this._terminalProcessProxies.get(terminalId); if (!terminal) { - throw new Error(`Unknown terminal: ${terminalId}`); + this._logService.error(`Unknown terminal: ${terminalId}`); + return undefined; } return terminal; } diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index 52185f8d..370f08df 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -3,12 +3,31 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { getTestSubscriptionKey, RunTestsRequest, RunTestsResult, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; -import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; -import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { ExtHostContext, ExtHostTestingResource, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { getTestSubscriptionKey, RunTestsRequest, RunTestsResult, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; +import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; +import { ExtHostContext, ExtHostTestingResource, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol'; + +const reviveDiff = (diff: TestsDiff) => { + for (const entry of diff) { + if (entry[0] === TestDiffOpType.Add || entry[0] === TestDiffOpType.Update) { + const item = entry[1]; + if (item.item.location) { + item.item.location.uri = URI.revive(item.item.location.uri); + } + + for (const message of item.item.state.messages) { + if (message.location) { + message.location.uri = URI.revive(message.location.uri); + } + } + } + } +}; @extHostNamedCustomer(MainContext.MainThreadTesting) export class MainThreadTesting extends Disposable implements MainThreadTestingShape { @@ -18,11 +37,28 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh constructor( extHostContext: IExtHostContext, @ITestService private readonly testService: ITestService, + @ITestResultService resultService: ITestResultService, ) { super(); this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostTesting); this._register(this.testService.onShouldSubscribe(args => this.proxy.$subscribeToTests(args.resource, args.uri))); this._register(this.testService.onShouldUnsubscribe(args => this.proxy.$unsubscribeFromTests(args.resource, args.uri))); + + const testCompleteListener = this._register(new MutableDisposable()); + this._register(resultService.onNewTestResult(results => { + testCompleteListener.value = results.onComplete(() => this.proxy.$publishTestResults({ tests: results.tests })); + })); + + testService.updateRootProviderCount(1); + + const lastCompleted = resultService.results.find(r => !r.isComplete); + if (lastCompleted) { + this.proxy.$publishTestResults({ tests: lastCompleted.tests }); + } + + for (const { resource, uri } of this.testService.subscriptions) { + this.proxy.$subscribeToTests(resource, uri); + } } /** @@ -30,7 +66,8 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh */ public $registerTestProvider(id: string) { this.testService.registerTestController(id, { - runTests: req => this.proxy.$runTestsForProvider(req), + runTests: (req, token) => this.proxy.$runTestsForProvider(req, token), + lookupTest: test => this.proxy.$lookupTest(test), }); } @@ -64,14 +101,19 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh * @inheritdoc */ public $publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void { + reviveDiff(diff); this.testService.publishDiff(resource, URI.revive(uri), diff); } - public $runTests(req: RunTestsRequest): Promise { - return this.testService.runTests(req); + public $runTests(req: RunTestsRequest, token: CancellationToken): Promise { + return this.testService.runTests(req, token); } public dispose() { - // no-op + this.testService.updateRootProviderCount(-1); + for (const subscription of this.testSubscriptions.values()) { + subscription.dispose(); + } + this.testSubscriptions.clear(); } } diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index 8e32acd7..cfe9d842 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -222,8 +222,8 @@ class TreeViewDataProvider implements ITreeViewDataProvider { const hasResolve = await this.hasResolve; if (elements) { for (const element of elements) { - const resolvable = new ResolvableTreeItem(element, hasResolve ? () => { - return this._proxy.$resolve(this.treeViewId, element.handle); + const resolvable = new ResolvableTreeItem(element, hasResolve ? (token) => { + return this._proxy.$resolve(this.treeViewId, element.handle, token); } : undefined); this.itemsMap.set(element.handle, resolvable); result.push(resolvable); diff --git a/src/vs/workbench/api/browser/mainThreadTunnelService.ts b/src/vs/workbench/api/browser/mainThreadTunnelService.ts index ac9b8356..82425aad 100644 --- a/src/vs/workbench/api/browser/mainThreadTunnelService.ts +++ b/src/vs/workbench/api/browser/mainThreadTunnelService.ts @@ -3,22 +3,31 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; import { MainThreadTunnelServiceShape, IExtHostContext, MainContext, ExtHostContext, ExtHostTunnelServiceShape } from 'vs/workbench/api/common/extHost.protocol'; import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { IRemoteExplorerService, makeAddress } from 'vs/workbench/services/remote/common/remoteExplorerService'; -import { ITunnelProvider, ITunnelService, TunnelCreationOptions, TunnelOptions } from 'vs/platform/remote/common/tunnel'; +import { CandidatePort, IRemoteExplorerService, makeAddress } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { ITunnelProvider, ITunnelService, TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions, RemoteTunnel, isPortPrivileged } from 'vs/platform/remote/common/tunnel'; import { Disposable } from 'vs/base/common/lifecycle'; import type { TunnelDescription } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { PORT_AUTO_FORWARD_SETTING } from 'vs/workbench/contrib/remote/browser/tunnelView'; +import { ILogService } from 'vs/platform/log/common/log'; @extHostNamedCustomer(MainContext.MainThreadTunnelService) export class MainThreadTunnelService extends Disposable implements MainThreadTunnelServiceShape { private readonly _proxy: ExtHostTunnelServiceShape; + private elevateionRetry: boolean = false; constructor( extHostContext: IExtHostContext, @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, - @ITunnelService private readonly tunnelService: ITunnelService + @ITunnelService private readonly tunnelService: ITunnelService, + @INotificationService private readonly notificationService: INotificationService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @ILogService private readonly logService: ILogService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTunnelService); @@ -26,14 +35,50 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun this._register(tunnelService.onTunnelClosed(() => this._proxy.$onDidTunnelsChange())); } + async $setCandidateFinder(): Promise { + if (this.remoteExplorerService.portsFeaturesEnabled) { + this._proxy.$registerCandidateFinder(this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING)); + } else { + this._register(this.remoteExplorerService.onEnabledPortsFeatures(() => this._proxy.$registerCandidateFinder(this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING)))); + } + this._register(this.configurationService.onDidChangeConfiguration(async (e) => { + if (e.affectsConfiguration(PORT_AUTO_FORWARD_SETTING)) { + return this._proxy.$registerCandidateFinder((this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING))); + } + })); + } + async $openTunnel(tunnelOptions: TunnelOptions, source: string): Promise { - const tunnel = await this.remoteExplorerService.forward(tunnelOptions.remoteAddress, tunnelOptions.localAddressPort, tunnelOptions.label, source); + const tunnel = await this.remoteExplorerService.forward(tunnelOptions.remoteAddress, tunnelOptions.localAddressPort, tunnelOptions.label, source, false); if (tunnel) { + if (!this.elevateionRetry + && (tunnelOptions.localAddressPort !== undefined) + && (tunnel.tunnelLocalPort !== undefined) + && isPortPrivileged(tunnelOptions.localAddressPort) + && (tunnel.tunnelLocalPort !== tunnelOptions.localAddressPort) + && this.tunnelService.canElevate) { + + this.elevationPrompt(tunnelOptions, tunnel, source); + } return TunnelDto.fromServiceTunnel(tunnel); } return undefined; } + private async elevationPrompt(tunnelOptions: TunnelOptions, tunnel: RemoteTunnel, source: string) { + return this.notificationService.prompt(Severity.Info, + nls.localize('remote.tunnel.openTunnel', "The extension {0} has forwarded port {1}. You'll need to run as superuser to use port {2} locally.", source, tunnelOptions.remoteAddress.port, tunnelOptions.localAddressPort), + [{ + label: nls.localize('remote.tunnelsView.elevationButton', "Use Port {0} as Sudo...", tunnel.tunnelRemotePort), + run: async () => { + this.elevateionRetry = true; + await this.remoteExplorerService.close({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }); + await this.remoteExplorerService.forward(tunnelOptions.remoteAddress, tunnelOptions.localAddressPort, tunnelOptions.label, source, true); + this.elevateionRetry = false; + } + }]); + } + async $closeTunnel(remote: { host: string, port: number }): Promise { return this.remoteExplorerService.close(remote); } @@ -47,27 +92,29 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun }); } - async $onFoundNewCandidates(candidates: { host: string, port: number, detail: string }[]): Promise { + async $onFoundNewCandidates(candidates: CandidatePort[]): Promise { this.remoteExplorerService.onFoundNewCandidates(candidates); } - async $tunnelServiceReady(): Promise { - return this.remoteExplorerService.restore(); - } - - async $setTunnelProvider(): Promise { + async $setTunnelProvider(features: TunnelProviderFeatures): Promise { const tunnelProvider: ITunnelProvider = { forwardPort: (tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions) => { const forward = this._proxy.$forwardPort(tunnelOptions, tunnelCreationOptions); if (forward) { return forward.then(tunnel => { + this.logService.trace(`MainThreadTunnelService: New tunnel established by tunnel provider: ${tunnel?.remoteAddress.host}:${tunnel?.remoteAddress.port}`); + if (!tunnel) { + return undefined; + } return { tunnelRemotePort: tunnel.remoteAddress.port, tunnelRemoteHost: tunnel.remoteAddress.host, localAddress: typeof tunnel.localAddress === 'string' ? tunnel.localAddress : makeAddress(tunnel.localAddress.host, tunnel.localAddress.port), tunnelLocalPort: typeof tunnel.localAddress !== 'string' ? tunnel.localAddress.port : undefined, - dispose: (silent?: boolean) => { - this._proxy.$closeTunnel({ host: tunnel.remoteAddress.host, port: tunnel.remoteAddress.port }, silent); + public: tunnel.public, + dispose: async (silent?: boolean) => { + this.logService.trace(`MainThreadTunnelService: Closing tunnel from tunnel provider: ${tunnel?.remoteAddress.host}:${tunnel?.remoteAddress.port}`); + return this._proxy.$closeTunnel({ host: tunnel.remoteAddress.host, port: tunnel.remoteAddress.port }, silent); } }; }); @@ -75,9 +122,16 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun return undefined; } }; - this.tunnelService.setTunnelProvider(tunnelProvider); + this.tunnelService.setTunnelProvider(tunnelProvider, features); } + async $setCandidateFilter(): Promise { + this.remoteExplorerService.setCandidateFilter((candidates: CandidatePort[]): Promise => { + return this._proxy.$applyCandidateFilter(candidates); + }); + } + + dispose(): void { } diff --git a/src/vs/workbench/api/browser/mainThreadUriOpeners.ts b/src/vs/workbench/api/browser/mainThreadUriOpeners.ts new file mode 100644 index 00000000..9ac5d76c --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadUriOpeners.ts @@ -0,0 +1,132 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Action } from 'vs/base/common/actions'; +import { isPromiseCanceledError } from 'vs/base/common/errors'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ExtHostContext, ExtHostUriOpenersShape, IExtHostContext, MainContext, MainThreadUriOpenersShape } from 'vs/workbench/api/common/extHost.protocol'; +import { defaultExternalUriOpenerId } from 'vs/workbench/contrib/externalUriOpener/common/configuration'; +import { ContributedExternalUriOpenersStore } from 'vs/workbench/contrib/externalUriOpener/common/contributedOpeners'; +import { IExternalOpenerProvider, IExternalUriOpener, IExternalUriOpenerService } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { extHostNamedCustomer } from '../common/extHostCustomers'; + +interface RegisteredOpenerMetadata { + readonly schemes: ReadonlySet; + readonly extensionId: ExtensionIdentifier; + readonly label: string; +} + +@extHostNamedCustomer(MainContext.MainThreadUriOpeners) +export class MainThreadUriOpeners extends Disposable implements MainThreadUriOpenersShape, IExternalOpenerProvider { + + private readonly proxy: ExtHostUriOpenersShape; + private readonly _registeredOpeners = new Map(); + private readonly _contributedExternalUriOpenersStore: ContributedExternalUriOpenersStore; + + constructor( + context: IExtHostContext, + @IStorageService storageService: IStorageService, + @IExternalUriOpenerService externalUriOpenerService: IExternalUriOpenerService, + @IExtensionService private readonly extensionService: IExtensionService, + @IOpenerService private readonly openerService: IOpenerService, + @INotificationService private readonly notificationService: INotificationService, + ) { + super(); + this.proxy = context.getProxy(ExtHostContext.ExtHostUriOpeners); + + this._register(externalUriOpenerService.registerExternalOpenerProvider(this)); + + this._contributedExternalUriOpenersStore = this._register(new ContributedExternalUriOpenersStore(storageService, extensionService)); + } + + public async *getOpeners(targetUri: URI): AsyncIterable { + + // Currently we only allow openers for http and https urls + if (targetUri.scheme !== Schemas.http && targetUri.scheme !== Schemas.https) { + return; + } + + await this.extensionService.activateByEvent(`onUriOpen:${targetUri.scheme}`); + + for (const [id, openerMetadata] of this._registeredOpeners) { + if (openerMetadata.schemes.has(targetUri.scheme)) { + yield this.createOpener(id, openerMetadata); + } + } + } + + private createOpener(id: string, metadata: RegisteredOpenerMetadata): IExternalUriOpener { + return { + id: id, + label: metadata.label, + canOpen: (uri, token) => { + return this.proxy.$canOpenUri(id, uri, token); + }, + openExternalUri: async (uri, ctx, token) => { + try { + await this.proxy.$openUri(id, { resolvedUri: uri, sourceUri: ctx.sourceUri }, token); + } catch (e) { + if (!isPromiseCanceledError(e)) { + const openDefaultAction = new Action('default', localize('openerFailedUseDefault', "Open using default opener"), undefined, undefined, () => { + return this.openerService.open(uri, { + allowTunneling: false, + allowContributedOpeners: defaultExternalUriOpenerId, + }); + }); + openDefaultAction.tooltip = uri.toString(); + + this.notificationService.notify({ + severity: Severity.Error, + message: localize('openerFailedMessage', 'Could not open uri with \'{0}\': {1}', id, e.toString()), + actions: { + primary: [ + openDefaultAction + ] + } + }); + } + } + return true; + }, + }; + } + + async $registerUriOpener( + id: string, + schemes: readonly string[], + extensionId: ExtensionIdentifier, + label: string, + ): Promise { + if (this._registeredOpeners.has(id)) { + throw new Error(`Opener with id '${id}' already registered`); + } + + this._registeredOpeners.set(id, { + schemes: new Set(schemes), + label, + extensionId, + }); + + this._contributedExternalUriOpenersStore.add(id, extensionId.value); + } + + async $unregisterUriOpener(id: string): Promise { + this._registeredOpeners.delete(id); + this._contributedExternalUriOpenersStore.delete(id); + } + + dispose(): void { + super.dispose(); + this._registeredOpeners.clear(); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts index 6428e6f4..7aa69077 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts @@ -129,6 +129,9 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc dispose(this._editorProviders.values()); this._editorProviders.clear(); + + dispose(this._revivers.values()); + this._revivers.clear(); } public get webviewInputs(): Iterable { return this._webviewInputs; } @@ -181,7 +184,6 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc webview.setName(value); } - public $setIconPath(handle: extHostProtocol.WebviewHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void { const webview = this.getWebviewInput(handle); webview.iconPath = reviveWebviewIcon(value); @@ -199,8 +201,7 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc } } - public $registerSerializer(viewType: string) - : void { + public $registerSerializer(viewType: string): void { if (this._revivers.has(viewType)) { throw new Error(`Reviver for ${viewType} already registered`); } @@ -216,7 +217,6 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc return; } - const handle = webviewInput.id; this.addWebviewInput(handle, webviewInput); diff --git a/src/vs/workbench/api/browser/mainThreadWebviews.ts b/src/vs/workbench/api/browser/mainThreadWebviews.ts index cc5b05c2..68409622 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviews.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviews.ts @@ -78,7 +78,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma private onDidClickLink(handle: extHostProtocol.WebviewHandle, link: string): void { const webview = this.getWebview(handle); if (this.isSupportedLink(webview, URI.parse(link))) { - this._openerService.open(link, { fromUserGesture: true }); + this._openerService.open(link, { fromUserGesture: true, allowContributedOpeners: true }); } } diff --git a/src/vs/workbench/api/browser/mainThreadWindow.ts b/src/vs/workbench/api/browser/mainThreadWindow.ts index 2a431111..7f10aa33 100644 --- a/src/vs/workbench/api/browser/mainThreadWindow.ts +++ b/src/vs/workbench/api/browser/mainThreadWindow.ts @@ -52,7 +52,11 @@ export class MainThreadWindow implements MainThreadWindowShape { // called with URI or transformed -> use uri target = uri; } - return this.openerService.open(target, { openExternal: true, allowTunneling: options.allowTunneling }); + return this.openerService.open(target, { + openExternal: true, + allowTunneling: options.allowTunneling, + allowContributedOpeners: options.allowContributedOpeners, + }); } async $asExternalUri(uriComponents: UriComponents, options: IOpenUriOptions): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/src/vs/workbench/api/browser/mainThreadWorkspace.ts index 252fa8e4..946c7bd3 100644 --- a/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/browser/mainThreadWorkspace.ts @@ -6,25 +6,25 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { isNative } from 'vs/base/common/platform'; +import { withNullAsUndefined } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { isNative } from 'vs/base/common/platform'; +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'; -import { IFileMatch, IPatternInfo, ISearchProgressItem, ISearchService } from 'vs/workbench/services/search/common/search'; -import { IWorkspaceContextService, WorkbenchState, IWorkspace, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IRequestService } from 'vs/platform/request/common/request'; +import { IWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { isUntitledWorkspace } from 'vs/platform/workspaces/common/workspaces'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { checkGlobFileExists } from 'vs/workbench/api/common/shared/workspaceContains'; import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IFileMatch, IPatternInfo, ISearchProgressItem, ISearchService } from 'vs/workbench/services/search/common/search'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; -import { ExtHostContext, ExtHostWorkspaceShape, IExtHostContext, MainContext, MainThreadWorkspaceShape, IWorkspaceData, ITextSearchComplete } from '../common/extHost.protocol'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { isUntitledWorkspace } from 'vs/platform/workspaces/common/workspaces'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { withNullAsUndefined } from 'vs/base/common/types'; -import { IFileService } from 'vs/platform/files/common/files'; -import { IRequestService } from 'vs/platform/request/common/request'; -import { checkGlobFileExists } from 'vs/workbench/api/common/shared/workspaceContains'; +import { ExtHostContext, ExtHostWorkspaceShape, IExtHostContext, ITextSearchComplete, IWorkspaceData, MainContext, MainThreadWorkspaceShape } from '../common/extHost.protocol'; @extHostNamedCustomer(MainContext.MainThreadWorkspace) export class MainThreadWorkspace implements MainThreadWorkspaceShape { @@ -139,7 +139,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { } const query = this._queryBuilder.file( - includeFolder ? [toWorkspaceFolder(includeFolder)] : workspace.folders, + includeFolder ? [includeFolder] : workspace.folders, { maxResults: withNullAsUndefined(maxResults), disregardExcludeSettings: (excludePatternOrDisregardExcludes === false) || undefined, diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 6be78f4e..84b95cfc 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -3,35 +3,32 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; +import { coalesce } from 'vs/base/common/arrays'; import { forEach } from 'vs/base/common/collections'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as resources from 'vs/base/common/resources'; -import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { ViewContainer, IViewsRegistry, ITreeViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, TEST_VIEW_CONTAINER_ID, IViewDescriptor, ViewContainerLocation, testViewIcon } from 'vs/workbench/common/views'; -import { CustomTreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { coalesce, } from 'vs/base/common/arrays'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { VIEWLET_ID as EXPLORER } from 'vs/workbench/contrib/files/common/files'; -import { VIEWLET_ID as SCM } from 'vs/workbench/contrib/scm/common/scm'; -import { VIEWLET_ID as DEBUG } from 'vs/workbench/contrib/debug/common/debug'; -import { VIEWLET_ID as REMOTE } from 'vs/workbench/contrib/remote/browser/remoteExplorer'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; -import { ViewletRegistry, Extensions as ViewletExtensions, ShowViewletAction } from 'vs/workbench/browser/viewlet'; -import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IWorkbenchActionRegistry, Extensions as ActionExtensions, CATEGORIES } from 'vs/workbench/common/actions'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { localize } from 'vs/nls'; +import { registerAction2 } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { WebviewViewPane } from 'vs/workbench/contrib/webviewView/browser/webviewViewPane'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { Registry } from 'vs/platform/registry/common/platform'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { CustomTreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { Extensions as ViewletExtensions, ShowViewletAction2, ViewletRegistry } from 'vs/workbench/browser/viewlet'; +import { CATEGORIES } from 'vs/workbench/common/actions'; +import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { Extensions as ViewContainerExtensions, ITreeViewDescriptor, IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; +import { VIEWLET_ID as DEBUG } from 'vs/workbench/contrib/debug/common/debug'; +import { VIEWLET_ID as EXPLORER } from 'vs/workbench/contrib/files/common/files'; +import { VIEWLET_ID as REMOTE } from 'vs/workbench/contrib/remote/browser/remoteExplorer'; +import { VIEWLET_ID as SCM } from 'vs/workbench/contrib/scm/common/scm'; +import { WebviewViewPane } from 'vs/workbench/contrib/webviewView/browser/webviewViewPane'; +import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; export interface IUserFriendlyViewsContainerDescriptor { id: string; @@ -110,7 +107,7 @@ const viewDescriptor: IJSONSchema = { defaultSnippets: [{ body: { id: '${1:id}', name: '${2:name}' } }], properties: { type: { - markdownDescription: localize('vscode.extension.contributes.view.type', "Type of the the view. This can either be `tree` for a tree view based view or `webview` for a webview based view. The default is `tree`."), + markdownDescription: localize('vscode.extension.contributes.view.type', "Type of the view. This can either be `tree` for a tree view based view or `webview` for a webview based view. The default is `tree`."), type: 'string', enum: [ 'tree', @@ -255,7 +252,8 @@ const viewsExtensionPoint: IExtensionPoint = ExtensionsR jsonSchema: viewsContribution }); -const TEST_VIEW_CONTAINER_ORDER = 6; +const CUSTOM_VIEWS_START_ORDER = 7; + class ViewsExtensionHandler implements IWorkbenchContribution { private viewContainersRegistry: IViewContainersRegistry; @@ -271,7 +269,6 @@ class ViewsExtensionHandler implements IWorkbenchContribution { } private handleAndRegisterCustomViewContainers() { - this.registerTestViewContainer(); viewsContainersExtensionPoint.setHandler((extensions, { added, removed }) => { if (removed.length) { this.removeCustomViewContainers(removed); @@ -284,7 +281,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { private addCustomViewContainers(extensionPoints: readonly IExtensionPointUser[], existingViewContainers: ViewContainer[]): void { const viewContainersRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); - let activityBarOrder = TEST_VIEW_CONTAINER_ORDER + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Sidebar).length + 1; + let activityBarOrder = CUSTOM_VIEWS_START_ORDER + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Sidebar).length; let panelOrder = 5 + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Panel).length + 1; for (let { value, collector, description } of extensionPoints) { forEach(value, entry => { @@ -318,13 +315,6 @@ class ViewsExtensionHandler implements IWorkbenchContribution { } } - private registerTestViewContainer(): void { - const title = localize('test', "Test"); - const icon = testViewIcon; - - this.registerCustomViewContainer(TEST_VIEW_CONTAINER_ID, title, icon, TEST_VIEW_CONTAINER_ORDER, undefined, ViewContainerLocation.Sidebar); - } - private isValidViewsContainer(viewsContainersDescriptors: IUserFriendlyViewsContainerDescriptor[], collector: ExtensionMessageCollector): boolean { if (!Array.isArray(viewsContainersDescriptors)) { collector.error(localize('viewcontainer requirearray', "views containers must be an array")); @@ -395,22 +385,15 @@ class ViewsExtensionHandler implements IWorkbenchContribution { }, location); // Register Action to Open Viewlet - class OpenCustomViewletAction extends ShowViewletAction { - constructor( - id: string, label: string, - @IViewletService viewletService: IViewletService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService - ) { - super(id, label, id, viewletService, editorGroupService, layoutService); + registerAction2(class OpenCustomViewletAction extends ShowViewletAction2 { + constructor() { + super({ id, f1: true, title: localize('showViewlet', "Show {0}", title), category: CATEGORIES.View.value }); } - } - const registry = Registry.as(ActionExtensions.WorkbenchActions); - registry.registerWorkbenchAction( - SyncActionDescriptor.create(OpenCustomViewletAction, id, localize('showViewlet', "Show {0}", title)), - `View: Show ${title}`, - CATEGORIES.View.value - ); + + protected viewletId() { + return id; + } + }); } return viewContainer; @@ -501,7 +484,8 @@ class ViewsExtensionHandler implements IWorkbenchContribution { originalContainerId: entry.key, group: item.group, remoteAuthority: item.remoteName || (item).remoteAuthority, // TODO@roblou - delete after remote extensions are updated - hideByDefault: initialVisibility === InitialVisibility.Hidden + hideByDefault: initialVisibility === InitialVisibility.Hidden, + workspace: viewContainer?.id === REMOTE ? true : undefined }; diff --git a/src/vs/workbench/api/common/apiCommands.ts b/src/vs/workbench/api/common/apiCommands.ts index 7670549c..d4a7ea40 100644 --- a/src/vs/workbench/api/common/apiCommands.ts +++ b/src/vs/workbench/api/common/apiCommands.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI, UriComponents } from 'vs/base/common/uri'; +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'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -12,7 +12,6 @@ import { IWorkspacesService, IRecent } from 'vs/platform/workspaces/common/works import { ILogService } from 'vs/platform/log/common/log'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IViewDescriptorService, IViewsService, ViewVisibilityState } from 'vs/workbench/common/views'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; // ----------------------------------------------------------------- // The following commands are registered on both sides separately. @@ -128,12 +127,6 @@ CommandsRegistry.registerCommand('_extensionTests.setLogLevel', function (access } }); -CommandsRegistry.registerCommand('_workbench.openExternal', function (accessor: ServicesAccessor, uri: UriComponents, options: { allowTunneling?: boolean }) { - // TODO: discuss martin, ben where to put this - const openerService = accessor.get(IOpenerService); - openerService.open(URI.revive(uri), options); -}); - CommandsRegistry.registerCommand('_extensionTests.getLogLevel', function (accessor: ServicesAccessor) { const logService = accessor.get(ILogService); diff --git a/src/vs/workbench/api/common/exHostSecretState.ts b/src/vs/workbench/api/common/exHostSecretState.ts new file mode 100644 index 00000000..b06f1245 --- /dev/null +++ b/src/vs/workbench/api/common/exHostSecretState.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ExtHostSecretStateShape, MainContext, MainThreadSecretStateShape } from 'vs/workbench/api/common/extHost.protocol'; +import { Emitter } from 'vs/base/common/event'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export class ExtHostSecretState implements ExtHostSecretStateShape { + private _proxy: MainThreadSecretStateShape; + private _onDidChangePassword = new Emitter<{ extensionId: string, key: string }>(); + readonly onDidChangePassword = this._onDidChangePassword.event; + + constructor(mainContext: IExtHostRpcService) { + this._proxy = mainContext.getProxy(MainContext.MainThreadSecretState); + } + + async $onDidChangePassword(e: { extensionId: string, key: string }): Promise { + this._onDidChangePassword.fire(e); + } + + get(extensionId: string, key: string): Promise { + return this._proxy.$getPassword(extensionId, key); + } + + store(extensionId: string, key: string, value: string): Promise { + return this._proxy.$setPassword(extensionId, key, value); + } + + delete(extensionId: string, key: string): Promise { + return this._proxy.$deletePassword(extensionId, key); + } +} + +export interface IExtHostSecretState extends ExtHostSecretState { } +export const IExtHostSecretState = createDecorator('IExtHostSecretState'); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index fa880fcf..2da02381 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -82,6 +82,9 @@ import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPane import { ExtHostBulkEdits } from 'vs/workbench/api/common/extHostBulkEdits'; import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; import { ExtHostTesting } from 'vs/workbench/api/common/extHostTesting'; +import { ExtHostUriOpeners } from 'vs/workbench/api/common/extHostUriOpener'; +import { IExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState'; +import { ExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -107,6 +110,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostTunnelService = accessor.get(IExtHostTunnelService); const extHostApiDeprecation = accessor.get(IExtHostApiDeprecationService); const extHostWindow = accessor.get(IExtHostWindow); + const extHostSecretState = accessor.get(IExtHostSecretState); // register addressable instances rpcProtocol.set(ExtHostContext.ExtHostFileSystemInfo, extHostFileSystemInfo); @@ -117,6 +121,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I rpcProtocol.set(ExtHostContext.ExtHostStorage, extHostStorage); rpcProtocol.set(ExtHostContext.ExtHostTunnelService, extHostTunnelService); rpcProtocol.set(ExtHostContext.ExtHostWindow, extHostWindow); + rpcProtocol.set(ExtHostContext.ExtHostSecretState, extHostSecretState); // automatically create and register addressable instances const extHostDecorations = rpcProtocol.set(ExtHostContext.ExtHostDecorations, accessor.get(IExtHostDecorations)); @@ -129,6 +134,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostOutputService = rpcProtocol.set(ExtHostContext.ExtHostOutputService, accessor.get(IExtHostOutputService)); // manually create and register addressable instances + const extHostEditorTabs = rpcProtocol.set(ExtHostContext.ExtHostEditorTabs, new ExtHostEditorTabs()); const extHostUrls = rpcProtocol.set(ExtHostContext.ExtHostUrls, new ExtHostUrls(rpcProtocol)); const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors)); const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors, extHostLogService)); @@ -154,6 +160,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews, extHostWebviewPanels)); const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews)); const extHostTesting = rpcProtocol.set(ExtHostContext.ExtHostTesting, new ExtHostTesting(rpcProtocol, extHostDocumentsAndEditors, extHostWorkspace)); + const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol)); // Check that no named customers are missing const expected: ProxyIdentifier[] = values(ExtHostContext); @@ -209,22 +216,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get onDidChangeSessions(): Event { return extHostAuthentication.onDidChangeSessions; }, - registerAuthenticationProvider(provider: vscode.AuthenticationProvider): vscode.Disposable { + registerAuthenticationProvider(id: string, label: string, provider: vscode.AuthenticationProvider, options?: vscode.AuthenticationProviderOptions): vscode.Disposable { checkProposedApiEnabled(extension); - return extHostAuthentication.registerAuthenticationProvider(provider); + return extHostAuthentication.registerAuthenticationProvider(id, label, provider, options); }, get onDidChangeAuthenticationProviders(): Event { checkProposedApiEnabled(extension); return extHostAuthentication.onDidChangeAuthenticationProviders; }, - getProviderIds(): Thenable> { - checkProposedApiEnabled(extension); - return extHostAuthentication.getProviderIds(); - }, - get providerIds(): string[] { - checkProposedApiEnabled(extension); - return extHostAuthentication.providerIds; - }, get providers(): ReadonlyArray { checkProposedApiEnabled(extension); return extHostAuthentication.providers; @@ -232,22 +231,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I logout(providerId: string, sessionId: string): Thenable { checkProposedApiEnabled(extension); return extHostAuthentication.logout(providerId, sessionId); - }, - getPassword(key: string): Thenable { - checkProposedApiEnabled(extension); - return extHostAuthentication.getPassword(extension, key); - }, - setPassword(key: string, value: string): Thenable { - checkProposedApiEnabled(extension); - return extHostAuthentication.setPassword(extension, key, value); - }, - deletePassword(key: string): Thenable { - checkProposedApiEnabled(extension); - return extHostAuthentication.deletePassword(extension, key); - }, - get onDidChangePassword(): Event { - checkProposedApiEnabled(extension); - return extHostAuthentication.onDidChangePassword; } }; @@ -311,8 +294,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get shell() { return extHostTerminalService.getDefaultShell(false, configProvider); }, - openExternal(uri: URI) { - return extHostWindow.openUri(uri, { allowTunneling: !!initData.remote.authority }); + openExternal(uri: URI, options?: { allowContributedOpeners?: boolean | string; }) { + return extHostWindow.openUri(uri, { + allowTunneling: !!initData.remote.authority, + allowContributedOpeners: options?.allowContributedOpeners, + }); }, asExternalUri(uri: URI) { if (uri.scheme === initData.environment.appUriScheme) { @@ -354,6 +340,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostTesting.runTests(provider); }, + get onDidChangeTestResults() { + checkProposedApiEnabled(extension); + return extHostTesting.onLastResultsChanged; + }, + get testResults() { + checkProposedApiEnabled(extension); + return extHostTesting.lastResults; + }, }; // namespace: extensions @@ -480,6 +474,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I getTokenInformationAtPosition(doc: vscode.TextDocument, pos: vscode.Position) { checkProposedApiEnabled(extension); return extHostLanguages.tokenAtPosition(doc, pos); + }, + registerInlineHintsProvider(selector: vscode.DocumentSelector, provider: vscode.InlineHintsProvider): vscode.Disposable { + checkProposedApiEnabled(extension); + return extHostLanguageFeatures.registerInlineHintsProvider(extension, selector, provider); } }; @@ -588,10 +586,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I id = extension.identifier.value; name = nls.localize('extensionLabel', "{0} (Extension)", extension.displayName || extension.name); alignment = alignmentOrOptions; - priority = priority; } - return extHostStatusBar.createStatusBarEntry(id, name, alignment, priority, accessibilityInformation, extension); + return extHostStatusBar.createStatusBarEntry(id, name, alignment, priority, accessibilityInformation); }, setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): vscode.Disposable { return extHostStatusBar.setStatusBarMessage(text, timeoutOrThenable); @@ -691,6 +688,18 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I showNotebookDocument(document, options?) { checkProposedApiEnabled(extension); return extHostNotebook.showNotebookDocument(document, options); + }, + registerExternalUriOpener(id: string, opener: vscode.ExternalUriOpener, metadata: vscode.ExternalUriOpenerMetadata) { + checkProposedApiEnabled(extension); + return extHostUriOpeners.registerExternalUriOpener(extension.identifier, id, opener, metadata); + }, + get openEditors() { + checkProposedApiEnabled(extension); + return extHostEditorTabs.tabs; + }, + get onDidChangeOpenEditors() { + checkProposedApiEnabled(extension); + return extHostEditorTabs.onDidChangeTabs; } }; @@ -1108,6 +1117,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I CallHierarchyIncomingCall: extHostTypes.CallHierarchyIncomingCall, CallHierarchyItem: extHostTypes.CallHierarchyItem, CallHierarchyOutgoingCall: extHostTypes.CallHierarchyOutgoingCall, + CancellationError: errors.CancellationError, CancellationTokenSource: CancellationTokenSource, CodeAction: extHostTypes.CodeAction, CodeActionKind: extHostTypes.CodeActionKind, @@ -1148,6 +1158,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I EventEmitter: Emitter, ExtensionKind: extHostTypes.ExtensionKind, ExtensionMode: extHostTypes.ExtensionMode, + ExternalUriOpenerPriority: extHostTypes.ExternalUriOpenerPriority, FileChangeType: extHostTypes.FileChangeType, FileDecoration: extHostTypes.FileDecoration, FileSystemError: extHostTypes.FileSystemError, @@ -1157,6 +1168,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I FunctionBreakpoint: extHostTypes.FunctionBreakpoint, Hover: extHostTypes.Hover, IndentAction: languageConfiguration.IndentAction, + InlineHint: extHostTypes.InlineHint, Location: extHostTypes.Location, MarkdownString: extHostTypes.MarkdownString, OverviewRulerLane: OverviewRulerLane, diff --git a/src/vs/workbench/api/common/extHost.common.services.ts b/src/vs/workbench/api/common/extHost.common.services.ts index 6c1a02f4..b1fe830b 100644 --- a/src/vs/workbench/api/common/extHost.common.services.ts +++ b/src/vs/workbench/api/common/extHost.common.services.ts @@ -21,6 +21,7 @@ import { IExtHostApiDeprecationService, ExtHostApiDeprecationService, } from 'vs import { IExtHostWindow, ExtHostWindow } from 'vs/workbench/api/common/extHostWindow'; import { IExtHostConsumerFileSystem, ExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer'; import { IExtHostFileSystemInfo, ExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; +import { IExtHostSecretState, ExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState'; registerSingleton(IExtensionStoragePaths, ExtensionStoragePaths); registerSingleton(IExtHostApiDeprecationService, ExtHostApiDeprecationService); @@ -39,3 +40,4 @@ registerSingleton(IExtHostTerminalService, WorkerExtHostTerminalService); registerSingleton(IExtHostTunnelService, ExtHostTunnelService); registerSingleton(IExtHostWindow, ExtHostWindow); registerSingleton(IExtHostWorkspace, ExtHostWorkspace); +registerSingleton(IExtHostSecretState, ExtHostSecretState); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index b97a962f..be7a14f8 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as performance from 'vs/base/common/performance'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IRemoteConsoleLog } from 'vs/base/common/console'; @@ -41,13 +42,13 @@ import { IRevealOptions, ITreeItem } from 'vs/workbench/common/views'; import { IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug'; import { ITextQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder'; import { ITerminalDimensions, IShellLaunchConfig, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; -import { ActivationKind, ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions'; +import { ActivationKind, ExtensionActivationError, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; import { createExtHostContextProxyIdentifier as createExtId, createMainContextProxyIdentifier as createMainId, IRPCProtocol } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import * as search from 'vs/workbench/services/search/common/search'; import { EditorGroupColumn, SaveReason } from 'vs/workbench/common/editor'; import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; -import { TunnelCreationOptions, TunnelOptions } from 'vs/platform/remote/common/tunnel'; +import { TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions } from 'vs/platform/remote/common/tunnel'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; import { revive } from 'vs/base/common/marshalling'; import { IProcessedOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEventDto, NotebookDataDto, IMainCellDto, INotebookDocumentFilter, INotebookKernelInfoDto2, TransientMetadata, INotebookCellStatusBarEntry, ICellRange, INotebookDecorationRenderOptions, INotebookExclusiveDocumentFilter } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -57,7 +58,8 @@ import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync'; -import { RunTestForProviderRequest, RunTestsRequest, RunTestsResult, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem, InternalTestResults, RunTestForProviderRequest, RunTestsRequest, RunTestsResult, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -108,7 +110,8 @@ export interface IConfigurationInitData extends IConfigurationData { } export interface IExtHostContext extends IRPCProtocol { - remoteAuthority: string | null; + readonly remoteAuthority: string | null; + readonly extensionHostKind: ExtensionHostKind; } export interface IMainContext extends IRPCProtocol { @@ -174,7 +177,9 @@ export interface MainThreadAuthenticationShape extends IDisposable { $getSessions(providerId: string): Promise>; $login(providerId: string, scopes: string[]): Promise; $logout(providerId: string, sessionId: string): Promise; +} +export interface MainThreadSecretStateShape extends IDisposable { $getPassword(extensionId: string, key: string): Promise; $setPassword(extensionId: string, key: string, value: string): Promise; $deletePassword(extensionId: string, key: string): Promise; @@ -330,7 +335,7 @@ export interface IIndentationRuleDto { export interface IOnEnterRuleDto { beforeText: IRegExpDto; afterText?: IRegExpDto; - oneLineAboveText?: IRegExpDto; + previousLineText?: IRegExpDto; action: EnterAction; } export interface ILanguageConfigurationDto { @@ -397,6 +402,8 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void; $registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void; $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void; + $registerInlineHintsProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void; + $emitInlineHintsEvent(eventHandle: number, event?: any): void; $registerDocumentLinkProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean): void; $registerDocumentColorProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerFoldingRangeProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void; @@ -415,6 +422,7 @@ export interface MainThreadLanguagesShape extends IDisposable { export interface MainThreadMessageOptions { extension?: IExtensionDescription; modal?: boolean; + useCustom?: boolean; } export interface MainThreadMessageServiceShape extends IDisposable { @@ -438,6 +446,16 @@ export interface MainThreadProgressShape extends IDisposable { $progressEnd(handle: number): void; } +/** + * A terminal that is created on the extension host side is temporarily assigned + * a UUID by the extension host that created it. Once the renderer side has assigned + * a real numeric id, the numeric id will be used. + * + * All other terminals (that are not created on the extension host side) always + * use the numeric id. + */ +export type TerminalIdentifier = number | string; + export interface TerminalLaunchConfig { name?: string; shellPath?: string; @@ -452,11 +470,11 @@ export interface TerminalLaunchConfig { } export interface MainThreadTerminalServiceShape extends IDisposable { - $createTerminal(config: TerminalLaunchConfig): Promise<{ id: number, name: string; }>; - $dispose(terminalId: number): void; - $hide(terminalId: number): void; - $sendText(terminalId: number, text: string, addNewLine: boolean): void; - $show(terminalId: number, preserveFocus: boolean): void; + $createTerminal(extHostTerminalId: string, config: TerminalLaunchConfig): Promise; + $dispose(id: TerminalIdentifier): void; + $hide(id: TerminalIdentifier): void; + $sendText(id: TerminalIdentifier, text: string, addNewLine: boolean): void; + $show(id: TerminalIdentifier, preserveFocus: boolean): void; $startSendingDataEvents(): void; $stopSendingDataEvents(): void; $startLinkProvider(): void; @@ -594,6 +612,24 @@ export interface ExtHostEditorInsetsShape { $onDidReceiveMessage(handle: number, message: any): void; } +//#region --- open editors model + +export interface MainThreadEditorTabsShape extends IDisposable { + // manage tabs: move, close, rearrange etc +} + +export interface IEditorTabDto { + group: number; + name: string; + resource: UriComponents +} + +export interface IExtHostEditorTabsShape { + $acceptEditorTabs(tabs: IEditorTabDto[]): void; +} + +//#endregion + export type WebviewHandle = string; export interface WebviewPanelShowOptions { @@ -739,6 +775,7 @@ export enum NotebookEditorRevealType { Default = 0, InCenter = 1, InCenterIfOutsideViewport = 2, + AtTop = 3 } export interface INotebookDocumentShowOptions { @@ -786,6 +823,16 @@ export interface ExtHostUrlsShape { $handleExternalUri(handle: number, uri: UriComponents): Promise; } +export interface MainThreadUriOpenersShape extends IDisposable { + $registerUriOpener(id: string, schemes: readonly string[], extensionId: ExtensionIdentifier, label: string): Promise; + $unregisterUriOpener(id: string): Promise; +} + +export interface ExtHostUriOpenersShape { + $canOpenUri(id: string, uri: UriComponents, token: CancellationToken): Promise; + $openUri(id: string, context: { resolvedUri: UriComponents, sourceUri: UriComponents }, token: CancellationToken): Promise; +} + export interface ITextSearchComplete { limitHit?: boolean; } @@ -853,6 +900,7 @@ export interface MainThreadExtensionServiceShape extends IDisposable { $onExtensionActivationError(extensionId: ExtensionIdentifier, error: ExtensionActivationError): Promise; $onExtensionRuntimeError(extensionId: ExtensionIdentifier, error: SerializedError): void; $onExtensionHostExit(code: number): Promise; + $setPerformanceMarks(marks: performance.PerformanceMark[]): Promise; } export interface SCMProviderFeatures { @@ -946,6 +994,7 @@ export interface MainThreadDebugServiceShape extends IDisposable { export interface IOpenUriOptions { readonly allowTunneling?: boolean; + readonly allowContributedOpeners?: boolean | string; } export interface MainThreadWindowShape extends IDisposable { @@ -958,8 +1007,9 @@ export interface MainThreadTunnelServiceShape extends IDisposable { $openTunnel(tunnelOptions: TunnelOptions, source: string | undefined): Promise; $closeTunnel(remote: { host: string, port: number }): Promise; $getTunnels(): Promise; - $setTunnelProvider(): Promise; - $tunnelServiceReady(): Promise; + $setTunnelProvider(features: TunnelProviderFeatures): Promise; + $setCandidateFinder(): Promise; + $setCandidateFilter(): Promise; $onFoundNewCandidates(candidates: { host: string, port: number, detail: string }[]): Promise; } @@ -1052,7 +1102,7 @@ export interface ExtHostTreeViewsShape { $setSelection(treeViewId: string, treeItemHandles: string[]): void; $setVisible(treeViewId: string, visible: boolean): void; $hasResolve(treeViewId: string): Promise; - $resolve(treeViewId: string, treeItemHandle: string): Promise; + $resolve(treeViewId: string, treeItemHandle: string, token: CancellationToken): Promise; } export interface ExtHostWorkspaceShape { @@ -1094,7 +1144,10 @@ export interface ExtHostAuthenticationShape { $onDidChangeAuthenticationSessions(id: string, label: string, event: modes.AuthenticationSessionsChangeEvent): Promise; $onDidChangeAuthenticationProviders(added: modes.AuthenticationProviderInformation[], removed: modes.AuthenticationProviderInformation[]): Promise; $setProviders(providers: modes.AuthenticationProviderInformation[]): Promise; - $onDidChangePassword(): Promise; +} + +export interface ExtHostSecretStateShape { + $onDidChangePassword(e: { extensionId: string, key: string }): Promise; } export interface ExtHostSearchShape { @@ -1145,9 +1198,14 @@ export interface SourceTargetPair { target: UriComponents; } +export interface IWillRunFileOperationParticipation { + edit: IWorkspaceEditDto; + extensionNames: string[] +} + export interface ExtHostFileSystemEventServiceShape { $onFileEvent(events: FileSystemEvents): void; - $onWillRunFileOperation(operation: files.FileOperation, files: SourceTargetPair[], undoRedoGroupId: number | undefined, timeout: number, token: CancellationToken): Promise; + $onWillRunFileOperation(operation: files.FileOperation, files: SourceTargetPair[], timeout: number, token: CancellationToken): Promise; $onDidRunFileOperation(operation: files.FileOperation, files: SourceTargetPair[]): void; } @@ -1252,6 +1310,18 @@ export interface ISignatureHelpContextDto { readonly activeSignatureHelp?: ISignatureHelpDto; } +export interface IInlineHintDto { + text: string; + range: IRange; + hoverMessage?: string; + whitespaceBefore?: boolean; + whitespaceAfter?: boolean; +} + +export interface IInlineHintsDto { + hints: IInlineHintDto[] +} + export interface ILocationDto { uri: UriComponents; range: IRange; @@ -1441,6 +1511,7 @@ export interface ExtHostLanguageFeaturesShape { $releaseCompletionItems(handle: number, id: number): void; $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, context: modes.SignatureHelpContext, token: CancellationToken): Promise; $releaseSignatureHelp(handle: number, id: number): void; + $provideInlineHints(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise $provideDocumentLinks(handle: number, resource: UriComponents, token: CancellationToken): Promise; $resolveDocumentLink(handle: number, id: ChainedCacheId, token: CancellationToken): Promise; $releaseDocumentLinks(handle: number, id: number): void; @@ -1473,6 +1544,7 @@ export interface IShellLaunchConfigDto { cwd?: string | UriComponents; env?: { [key: string]: string | null; }; hideFromUser?: boolean; + flowControl?: boolean; } export interface IShellDefinitionDto { @@ -1503,7 +1575,7 @@ export interface ITerminalDimensionsDto { export interface ExtHostTerminalServiceShape { $acceptTerminalClosed(id: number, exitCode: number | undefined): void; - $acceptTerminalOpened(id: number, name: string, shellLaunchConfig: IShellLaunchConfigDto): void; + $acceptTerminalOpened(id: number, extHostTerminalId: string | undefined, name: string, shellLaunchConfig: IShellLaunchConfigDto): void; $acceptActiveTerminalChanged(id: number | null): void; $acceptTerminalProcessId(id: number, processId: number): void; $acceptTerminalProcessData(id: number, data: string): void; @@ -1512,6 +1584,7 @@ export interface ExtHostTerminalServiceShape { $acceptTerminalMaximumDimensions(id: number, cols: number, rows: number): void; $spawnExtHostProcess(id: number, shellLaunchConfig: IShellLaunchConfigDto, activeWorkspaceRootUri: UriComponents | undefined, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise; $startExtensionTerminal(id: number, initialDimensions: ITerminalDimensionsDto | undefined): Promise; + $acceptProcessAckDataEvent(id: number, charCount: number): void; $acceptProcessInput(id: number, data: string): void; $acceptProcessResize(id: number, cols: number, rows: number): void; $acceptProcessShutdown(id: number, immediate: boolean): void; @@ -1725,7 +1798,7 @@ export interface ExtHostNotebookShape { $saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise; $backup(viewType: string, uri: UriComponents, cancellation: CancellationToken): Promise; $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void; - $acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelId: string | undefined }): void; + $acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelFriendlyId: string | undefined }): void; $onDidReceiveMessage(editorId: string, rendererId: string | undefined, message: unknown): void; $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEventDto, isDirty: boolean): void; $acceptModelSaved(uriComponents: UriComponents): void; @@ -1749,9 +1822,11 @@ export interface MainThreadThemingShape extends IDisposable { } export interface ExtHostTunnelServiceShape { - $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise | undefined; + $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise; $closeTunnel(remote: { host: string, port: number }, silent?: boolean): Promise; $onDidTunnelsChange(): Promise; + $registerCandidateFinder(enable: boolean): Promise; + $applyCandidateFilter(candidates: CandidatePort[]): Promise; } export interface ExtHostTimelineShape { @@ -1764,11 +1839,12 @@ export const enum ExtHostTestingResource { } export interface ExtHostTestingShape { - $runTestsForProvider(req: RunTestForProviderRequest): Promise; + $runTestsForProvider(req: RunTestForProviderRequest, token: CancellationToken): Promise; $subscribeToTests(resource: ExtHostTestingResource, uri: UriComponents): void; $unsubscribeFromTests(resource: ExtHostTestingResource, uri: UriComponents): void; - + $lookupTest(test: TestIdWithProvider): Promise; $acceptDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void; + $publishTestResults(results: InternalTestResults): void; } export interface MainThreadTestingShape { @@ -1777,7 +1853,7 @@ export interface MainThreadTestingShape { $subscribeToDiffs(resource: ExtHostTestingResource, uri: UriComponents): void; $unsubscribeFromDiffs(resource: ExtHostTestingResource, uri: UriComponents): void; $publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void; - $runTests(req: RunTestsRequest): Promise; + $runTests(req: RunTestsRequest, token: CancellationToken): Promise; } // --- proxy identifiers @@ -1798,6 +1874,7 @@ export const MainContext = { MainThreadDocumentContentProviders: createMainId('MainThreadDocumentContentProviders'), MainThreadTextEditors: createMainId('MainThreadTextEditors'), MainThreadEditorInsets: createMainId('MainThreadEditorInsets'), + MainThreadEditorTabs: createMainId('MainThreadEditorTabs'), MainThreadErrors: createMainId('MainThreadErrors'), MainThreadTreeViews: createMainId('MainThreadTreeViews'), MainThreadDownloadService: createMainId('MainThreadDownloadService'), @@ -1810,6 +1887,7 @@ export const MainContext = { MainThreadProgress: createMainId('MainThreadProgress'), MainThreadQuickOpen: createMainId('MainThreadQuickOpen'), MainThreadStatusBar: createMainId('MainThreadStatusBar'), + MainThreadSecretState: createMainId('MainThreadSecretState'), MainThreadStorage: createMainId('MainThreadStorage'), MainThreadTelemetry: createMainId('MainThreadTelemetry'), MainThreadTerminalService: createMainId('MainThreadTerminalService'), @@ -1818,6 +1896,7 @@ export const MainContext = { MainThreadWebviewViews: createMainId('MainThreadWebviewViews'), MainThreadCustomEditors: createMainId('MainThreadCustomEditors'), MainThreadUrls: createMainId('MainThreadUrls'), + MainThreadUriOpeners: createMainId('MainThreadUriOpeners'), MainThreadWorkspace: createMainId('MainThreadWorkspace'), MainThreadFileSystem: createMainId('MainThreadFileSystem'), MainThreadExtensionService: createMainId('MainThreadExtensionService'), @@ -1863,10 +1942,13 @@ export const ExtHostContext = { ExtHostCustomEditors: createExtId('ExtHostCustomEditors'), ExtHostWebviewViews: createExtId('ExtHostWebviewViews'), ExtHostEditorInsets: createExtId('ExtHostEditorInsets'), + ExtHostEditorTabs: createExtId('ExtHostEditorTabs'), ExtHostProgress: createMainId('ExtHostProgress'), ExtHostComments: createMainId('ExtHostComments'), + ExtHostSecretState: createMainId('ExtHostSecretState'), ExtHostStorage: createMainId('ExtHostStorage'), ExtHostUrls: createExtId('ExtHostUrls'), + ExtHostUriOpeners: createExtId('ExtHostUriOpeners'), ExtHostOutputService: createMainId('ExtHostOutputService'), ExtHosLabelService: createMainId('ExtHostLabelService'), ExtHostNotebook: createMainId('ExtHostNotebook'), diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 08b2543a..93f77a95 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -20,6 +20,8 @@ import { IRange } from 'vs/editor/common/core/range'; import { IPosition } from 'vs/editor/common/core/position'; import { TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; //#region --- NEW world @@ -176,6 +178,57 @@ const newCommands: ApiCommand[] = [ [ApiCommandArgument.Uri, ApiCommandArgument.Number.with('linkResolveCount', 'Number of links that should be resolved, only when links are unresolved.').optional()], new ApiCommandResult('A promise that resolves to an array of DocumentLink-instances.', value => value.map(typeConverters.DocumentLink.to)) ), + // --- semantic tokens + new ApiCommand( + 'vscode.provideDocumentSemanticTokensLegend', '_provideDocumentSemanticTokensLegend', 'Provide semantic tokens legend for a document', + [ApiCommandArgument.Uri], + new ApiCommandResult('A promise that resolves to SemanticTokensLegend.', value => { + if (!value) { + return undefined; + } + return new types.SemanticTokensLegend(value.tokenTypes, value.tokenModifiers); + }) + ), + new ApiCommand( + 'vscode.provideDocumentSemanticTokens', '_provideDocumentSemanticTokens', 'Provide semantic tokens for a document', + [ApiCommandArgument.Uri], + new ApiCommandResult('A promise that resolves to SemanticTokens.', value => { + if (!value) { + return undefined; + } + const semanticTokensDto = decodeSemanticTokensDto(value); + if (semanticTokensDto.type !== 'full') { + // only accepting full semantic tokens from provideDocumentSemanticTokens + return undefined; + } + return new types.SemanticTokens(semanticTokensDto.data, undefined); + }) + ), + new ApiCommand( + 'vscode.provideDocumentRangeSemanticTokensLegend', '_provideDocumentRangeSemanticTokensLegend', 'Provide semantic tokens legend for a document range', + [ApiCommandArgument.Uri], + new ApiCommandResult('A promise that resolves to SemanticTokensLegend.', value => { + if (!value) { + return undefined; + } + return new types.SemanticTokensLegend(value.tokenTypes, value.tokenModifiers); + }) + ), + new ApiCommand( + 'vscode.provideDocumentRangeSemanticTokens', '_provideDocumentRangeSemanticTokens', 'Provide semantic tokens for a document range', + [ApiCommandArgument.Uri, ApiCommandArgument.Range], + new ApiCommandResult('A promise that resolves to SemanticTokens.', value => { + if (!value) { + return undefined; + } + const semanticTokensDto = decodeSemanticTokensDto(value); + if (semanticTokensDto.type !== 'full') { + // only accepting full semantic tokens from provideDocumentRangeSemanticTokens + return undefined; + } + return new types.SemanticTokens(semanticTokensDto.data, undefined); + }) + ), // --- completions new ApiCommand( 'vscode.executeCompletionItemProvider', '_executeCompletionItemProvider', 'Execute completion item provider.', @@ -271,13 +324,21 @@ const newCommands: ApiCommand[] = [ return []; }) ), + // --- inline hints + new ApiCommand( + 'vscode.executeInlineHintProvider', '_executeInlineHintProvider', 'Execute inline hints provider', + [ApiCommandArgument.Uri, ApiCommandArgument.Range], + new ApiCommandResult('A promise that resolves to an array of InlineHint objects', result => { + return result.map(typeConverters.InlineHint.to); + }) + ), // --- notebooks new ApiCommand( 'vscode.resolveNotebookContentProviders', '_resolveNotebookContentProvider', 'Resolve Notebook Content Providers', [ - new ApiCommandArgument('viewType', '', v => typeof v === 'string', v => v), - new ApiCommandArgument('displayName', '', v => typeof v === 'string', v => v), - new ApiCommandArgument('options', '', v => typeof v === 'object', v => v), + // new ApiCommandArgument('viewType', '', v => typeof v === 'string', v => v), + // new ApiCommandArgument('displayName', '', v => typeof v === 'string', v => v), + // new ApiCommandArgument('options', '', v => typeof v === 'object', v => v), ], new ApiCommandResult<{ viewType: string; diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index 63199906..7376e9c6 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -15,11 +15,15 @@ interface GetSessionsRequest { result: Promise; } +interface ProviderWithMetadata { + label: string; + provider: vscode.AuthenticationProvider; + options: vscode.AuthenticationProviderOptions; +} + export class ExtHostAuthentication implements ExtHostAuthenticationShape { private _proxy: MainThreadAuthenticationShape; - private _authenticationProviders: Map = new Map(); - - private _providerIds: string[] = []; + private _authenticationProviders: Map = new Map(); private _providers: vscode.AuthenticationProviderInformation[] = []; @@ -29,9 +33,6 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { private _onDidChangeSessions = new Emitter(); readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; - private _onDidChangePassword = new Emitter(); - readonly onDidChangePassword: Event = this._onDidChangePassword.event; - private _inFlightRequests = new Map(); constructor(mainContext: IMainContext) { @@ -43,14 +44,6 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { return Promise.resolve(); } - getProviderIds(): Promise> { - return this._proxy.$getProviderIds(); - } - - get providerIds(): string[] { - return this._providerIds; - } - get providers(): ReadonlyArray { return Object.freeze(this._providers.slice()); } @@ -90,128 +83,120 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { private async _getSession(requestingExtension: IExtensionDescription, extensionId: string, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions = {}): Promise { await this._proxy.$ensureProvider(providerId); - const provider = this._authenticationProviders.get(providerId); + const providerData = this._authenticationProviders.get(providerId); const extensionName = requestingExtension.displayName || requestingExtension.name; - if (!provider) { + if (!providerData) { return this._proxy.$getSession(providerId, scopes, extensionId, extensionName, options); } const orderedScopes = scopes.sort().join(' '); - const sessions = (await provider.getSessions()).filter(session => session.scopes.slice().sort().join(' ') === orderedScopes); + const sessions = (await providerData.provider.getSessions()).filter(session => session.scopes.slice().sort().join(' ') === orderedScopes); let session: vscode.AuthenticationSession | undefined = undefined; if (sessions.length) { - if (!provider.supportsMultipleAccounts) { + if (!providerData.options.supportsMultipleAccounts) { session = sessions[0]; - const allowed = await this._proxy.$getSessionsPrompt(providerId, session.account.label, provider.label, extensionId, extensionName); + const allowed = await this._proxy.$getSessionsPrompt(providerId, session.account.label, providerData.label, extensionId, extensionName); if (!allowed) { throw new Error('User did not consent to login.'); } } else { // On renderer side, confirm consent, ask user to choose between accounts if multiple sessions are valid - const selected = await this._proxy.$selectSession(providerId, provider.label, extensionId, extensionName, sessions, scopes, !!options.clearSessionPreference); + const selected = await this._proxy.$selectSession(providerId, providerData.label, extensionId, extensionName, sessions, scopes, !!options.clearSessionPreference); session = sessions.find(session => session.id === selected.id); } } else { if (options.createIfNone) { - const isAllowed = await this._proxy.$loginPrompt(provider.label, extensionName); + const isAllowed = await this._proxy.$loginPrompt(providerData.label, extensionName); if (!isAllowed) { throw new Error('User did not consent to login.'); } - session = await provider.login(scopes); + session = await providerData.provider.login(scopes); await this._proxy.$setTrustedExtensionAndAccountPreference(providerId, session.account.label, extensionId, extensionName, session.id); } else { await this._proxy.$requestNewSession(providerId, scopes, extensionId, extensionName); } } - return session; } async logout(providerId: string, sessionId: string): Promise { - const provider = this._authenticationProviders.get(providerId); - if (!provider) { + const providerData = this._authenticationProviders.get(providerId); + if (!providerData) { return this._proxy.$logout(providerId, sessionId); } - return provider.logout(sessionId); + return providerData.provider.logout(sessionId); } - registerAuthenticationProvider(provider: vscode.AuthenticationProvider): vscode.Disposable { - if (this._authenticationProviders.get(provider.id)) { - throw new Error(`An authentication provider with id '${provider.id}' is already registered.`); + registerAuthenticationProvider(id: string, label: string, provider: vscode.AuthenticationProvider, options?: vscode.AuthenticationProviderOptions): vscode.Disposable { + if (this._authenticationProviders.get(id)) { + throw new Error(`An authentication provider with id '${id}' is already registered.`); } - this._authenticationProviders.set(provider.id, provider); - if (!this._providerIds.includes(provider.id)) { - this._providerIds.push(provider.id); - } + this._authenticationProviders.set(id, { label, provider, options: options ?? { supportsMultipleAccounts: false } }); - if (!this._providers.find(p => p.id === provider.id)) { + if (!this._providers.find(p => p.id === id)) { this._providers.push({ - id: provider.id, - label: provider.label + id: id, + label: label }); } const listener = provider.onDidChangeSessions(e => { - this._proxy.$sendDidChangeSessions(provider.id, e); + this._proxy.$sendDidChangeSessions(id, e); }); - this._proxy.$registerAuthenticationProvider(provider.id, provider.label, provider.supportsMultipleAccounts); + this._proxy.$registerAuthenticationProvider(id, label, options?.supportsMultipleAccounts ?? false); return new Disposable(() => { listener.dispose(); - this._authenticationProviders.delete(provider.id); - const index = this._providerIds.findIndex(id => id === provider.id); - if (index > -1) { - this._providerIds.splice(index); - } + this._authenticationProviders.delete(id); - const i = this._providers.findIndex(p => p.id === provider.id); + const i = this._providers.findIndex(p => p.id === id); if (i > -1) { this._providers.splice(i); } - this._proxy.$unregisterAuthenticationProvider(provider.id); + this._proxy.$unregisterAuthenticationProvider(id); }); } $login(providerId: string, scopes: string[]): Promise { - const authProvider = this._authenticationProviders.get(providerId); - if (authProvider) { - return Promise.resolve(authProvider.login(scopes)); + const providerData = this._authenticationProviders.get(providerId); + if (providerData) { + return Promise.resolve(providerData.provider.login(scopes)); } throw new Error(`Unable to find authentication provider with handle: ${providerId}`); } $logout(providerId: string, sessionId: string): Promise { - const authProvider = this._authenticationProviders.get(providerId); - if (authProvider) { - return Promise.resolve(authProvider.logout(sessionId)); + const providerData = this._authenticationProviders.get(providerId); + if (providerData) { + return Promise.resolve(providerData.provider.logout(sessionId)); } throw new Error(`Unable to find authentication provider with handle: ${providerId}`); } $getSessions(providerId: string): Promise> { - const authProvider = this._authenticationProviders.get(providerId); - if (authProvider) { - return Promise.resolve(authProvider.getSessions()); + const providerData = this._authenticationProviders.get(providerId); + if (providerData) { + return Promise.resolve(providerData.provider.getSessions()); } throw new Error(`Unable to find authentication provider with handle: ${providerId}`); } async $getSessionAccessToken(providerId: string, sessionId: string): Promise { - const authProvider = this._authenticationProviders.get(providerId); - if (authProvider) { - const sessions = await authProvider.getSessions(); + const providerData = this._authenticationProviders.get(providerId); + if (providerData) { + const sessions = await providerData.provider.getSessions(); const session = sessions.find(session => session.id === sessionId); if (session) { return session.accessToken; @@ -245,23 +230,4 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { this._onDidChangeAuthenticationProviders.fire({ added, removed }); return Promise.resolve(); } - - async $onDidChangePassword(): Promise { - this._onDidChangePassword.fire(); - } - - getPassword(requestingExtension: IExtensionDescription, key: string): Promise { - const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); - return this._proxy.$getPassword(extensionId, key); - } - - setPassword(requestingExtension: IExtensionDescription, key: string, value: string): Promise { - const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); - return this._proxy.$setPassword(extensionId, key, value); - } - - deletePassword(requestingExtension: IExtensionDescription, key: string): Promise { - const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); - return this._proxy.$deletePassword(extensionId, key); - } } diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index 649612e3..c3373724 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -103,7 +103,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { const internalArgs = apiCommand.args.map((arg, i) => { if (!arg.validate(apiArgs[i])) { - throw new Error(`Invalid argument '${arg.name}' when running '${apiCommand.id}', receieved: ${apiArgs[i]}`); + throw new Error(`Invalid argument '${arg.name}' when running '${apiCommand.id}', received: ${apiArgs[i]}`); } return arg.convert(apiArgs[i]); }); @@ -194,7 +194,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { } } - private _executeContributedCommand(id: string, args: any[]): Promise { + private async _executeContributedCommand(id: string, args: any[]): Promise { const command = this._commands.get(id); if (!command) { throw new Error('Unknown command'); @@ -205,17 +205,16 @@ export class ExtHostCommands implements ExtHostCommandsShape { try { validateConstraint(args[i], description.args[i].constraint); } catch (err) { - return Promise.reject(new Error(`Running the contributed command: '${id}' failed. Illegal argument '${description.args[i].name}' - ${description.args[i].description}`)); + throw new Error(`Running the contributed command: '${id}' failed. Illegal argument '${description.args[i].name}' - ${description.args[i].description}`); } } } try { - const result = callback.apply(thisArg, args); - return Promise.resolve(result); + return await callback.apply(thisArg, args); } catch (err) { this._logService.error(err, id); - return Promise.reject(new Error(`Running the contributed command: '${id}' failed.`)); + throw new Error(`Running the contributed command: '${id}' failed.`); } } diff --git a/src/vs/workbench/api/common/extHostCustomEditors.ts b/src/vs/workbench/api/common/extHostCustomEditors.ts index becd0887..a4359d91 100644 --- a/src/vs/workbench/api/common/extHostCustomEditors.ts +++ b/src/vs/workbench/api/common/extHostCustomEditors.ts @@ -13,6 +13,7 @@ import * as modes from 'vs/editor/common/modes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; +import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { ExtHostWebviews, toExtensionData } from 'vs/workbench/api/common/extHostWebview'; import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels'; import { EditorGroupColumn } from 'vs/workbench/common/editor'; @@ -178,7 +179,7 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor options: { webviewOptions?: vscode.WebviewPanelOptions, supportsMultipleEditorsPerDocument?: boolean }, ): vscode.Disposable { const disposables = new DisposableStore(); - if ('resolveCustomTextEditor' in provider) { + if (isCustomTextEditorProvider(provider)) { disposables.add(this._editorProviders.addTextProvider(viewType, extension, provider)); this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, { supportsMove: !!provider.moveCustomTextEditor, @@ -208,7 +209,6 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor })); } - async $createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, cancellation: CancellationToken) { const entry = this._editorProviders.get(viewType); if (!entry) { @@ -261,8 +261,10 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor throw new Error(`No provider found for '${viewType}'`); } + const viewColumn = typeConverters.ViewColumn.to(position); + const webview = this._extHostWebview.createNewWebview(handle, options, entry.extension); - const panel = this._extHostWebviewPanels.createNewWebviewPanel(handle, viewType, title, position, options, webview); + const panel = this._extHostWebviewPanels.createNewWebviewPanel(handle, viewType, title, viewColumn, options, webview); const revivedResource = URI.revive(resource); @@ -350,7 +352,6 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor return backup.id; } - private getCustomDocumentEntry(viewType: string, resource: UriComponents): CustomDocumentStoreEntry { const entry = this._documents.get(viewType, URI.revive(resource)); if (!entry) { @@ -375,6 +376,9 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor } } +function isCustomTextEditorProvider(provider: vscode.CustomReadonlyEditorProvider | vscode.CustomTextEditorProvider): provider is vscode.CustomTextEditorProvider { + return typeof (provider as vscode.CustomTextEditorProvider).resolveCustomTextEditor === 'function'; +} function isEditEvent(e: vscode.CustomDocumentContentChangeEvent | vscode.CustomDocumentEditEvent): e is vscode.CustomDocumentEditEvent { return typeof (e as vscode.CustomDocumentEditEvent).undo === 'function' diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index 4026a1d2..d639431a 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -152,7 +152,7 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E const source = src; - if (typeof source.sourceReference === 'number') { + if (typeof source.sourceReference === 'number' && source.sourceReference > 0) { // src can be retrieved via DAP's "source" request let debug = `debug:${encodeURIComponent(source.path || '')}`; diff --git a/src/vs/workbench/api/common/extHostEditorTabs.ts b/src/vs/workbench/api/common/extHostEditorTabs.ts new file mode 100644 index 00000000..2d0f7b4c --- /dev/null +++ b/src/vs/workbench/api/common/extHostEditorTabs.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import { IEditorTabDto, IExtHostEditorTabsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { URI } from 'vs/base/common/uri'; +import { Emitter, Event } from 'vs/base/common/event'; + + +export interface IEditorTab { + name: string; + group: number; + resource: vscode.Uri +} + +export class ExtHostEditorTabs implements IExtHostEditorTabsShape { + + private readonly _onDidChangeTabs = new Emitter(); + readonly onDidChangeTabs: Event = this._onDidChangeTabs.event; + + private _tabs: IEditorTab[] = []; + + get tabs(): readonly IEditorTab[] { + return this._tabs; + } + + $acceptEditorTabs(tabs: IEditorTabDto[]): void { + this._tabs = tabs.map(dto => { + return { + name: dto.name, + group: dto.group, + resource: URI.revive(dto.resource) + }; + }); + this._onDidChangeTabs.fire(); + } +} diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 328b9327..2274a4f1 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -5,6 +5,7 @@ import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; +import * as performance from 'vs/base/common/performance'; import { originalFSPath, joinPath } from 'vs/base/common/resources'; import { Barrier, timeout } from 'vs/base/common/async'; import { dispose, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; @@ -35,6 +36,8 @@ import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelServ import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; import { Emitter, Event } from 'vs/base/common/event'; import { IExtensionActivationHost, checkActivateWorkspaceContainsExtension } from 'vs/workbench/api/common/shared/workspaceContains'; +import { ExtHostSecretState, IExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState'; +import { ExtensionSecrets } from 'vs/workbench/api/common/extHostSecrets'; interface ITestRunner { /** Old test runner API, as exported from `vscode/lib/testrunner` */ @@ -50,7 +53,7 @@ export const IHostUtils = createDecorator('IHostUtils'); export interface IHostUtils { readonly _serviceBrand: undefined; - exit(code?: number): void; + exit(code: number): void; exists(path: string): Promise; realpath(path: string): Promise; } @@ -94,6 +97,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme private readonly _readyToRunExtensions: Barrier; protected readonly _registry: ExtensionDescriptionRegistry; private readonly _storage: ExtHostStorage; + private readonly _secretState: ExtHostSecretState; private readonly _storagePath: IExtensionStoragePaths; private readonly _activator: ExtensionsActivator; private _extensionPathIndex: Promise> | null; @@ -115,7 +119,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme @IExtHostInitDataService initData: IExtHostInitDataService, @IExtensionStoragePaths storagePath: IExtensionStoragePaths, @IExtHostTunnelService extHostTunnelService: IExtHostTunnelService, - @IExtHostTerminalService extHostTerminalService: IExtHostTerminalService + @IExtHostTerminalService extHostTerminalService: IExtHostTerminalService, ) { super(); this._hostUtils = hostUtils; @@ -138,10 +142,12 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme this._readyToRunExtensions = new Barrier(); this._registry = new ExtensionDescriptionRegistry(this._initData.extensions); this._storage = new ExtHostStorage(this._extHostContext); + this._secretState = new ExtHostSecretState(this._extHostContext); this._storagePath = storagePath; this._instaService = instaService.createChild(new ServiceCollection( - [IExtHostStorage, this._storage] + [IExtHostStorage, this._storage], + [IExtHostSecretState, this._secretState] )); const hostExtensions = new Set(); @@ -184,6 +190,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme this._almostReadyToRunExtensions.open(); await this._extHostWorkspace.waitForInitializeCall(); + performance.mark('code/extHost/ready'); this._readyToStartExtensionHost.open(); if (this._initData.autoStart) { @@ -362,10 +369,14 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme const activationTimesBuilder = new ExtensionActivationTimesBuilder(reason.startup); return Promise.all([ - this._loadCommonJSModule(joinPath(extensionDescription.extensionLocation, entryPoint), activationTimesBuilder), + this._loadCommonJSModule(extensionDescription.identifier, joinPath(extensionDescription.extensionLocation, entryPoint), activationTimesBuilder), this._loadExtensionContext(extensionDescription) ]).then(values => { + performance.mark(`code/extHost/willActivateExtension/${extensionDescription.identifier.value}`); return AbstractExtHostExtensionService._callActivate(this._logService, extensionDescription.identifier, values[0], values[1], activationTimesBuilder); + }).then((activatedExtension) => { + performance.mark(`code/extHost/didActivateExtension/${extensionDescription.identifier.value}`); + return activatedExtension; }); } @@ -373,6 +384,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme const globalState = new ExtensionGlobalMemento(extensionDescription, this._storage); const workspaceState = new ExtensionMemento(extensionDescription.identifier.value, false, this._storage); + const secrets = new ExtensionSecrets(extensionDescription, this._secretState); const extensionMode = extensionDescription.isUnderDevelopment ? (this._initData.environment.extensionTestsLocationURI ? ExtensionMode.Test : ExtensionMode.Development) : ExtensionMode.Production; @@ -388,6 +400,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme return Object.freeze({ globalState, workspaceState, + secrets, subscriptions: [], get extensionUri() { return extensionDescription.extensionLocation; }, get extensionPath() { return extensionDescription.extensionLocation.fsPath; }, @@ -456,6 +469,9 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } private _activateAllStartupFinished(): void { + // startup is considered finished + this._mainThreadExtensionsProxy.$setPerformanceMarks(performance.getMarks()); + for (const desc of this._registry.getAllExtensionDescriptions()) { if (desc.activationEvents) { for (const activationEvent of desc.activationEvents) { @@ -541,7 +557,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme let testRunner: ITestRunner | INewTestRunner | undefined; let requireError: Error | undefined; try { - testRunner = await this._loadCommonJSModule(URI.file(extensionTestsPath), new ExtensionActivationTimesBuilder(false)); + testRunner = await this._loadCommonJSModule(null, URI.file(extensionTestsPath), new ExtensionActivationTimesBuilder(false)); } catch (error) { requireError = error; } @@ -586,11 +602,17 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } private _testRunnerExit(code: number): void { + this._logService.info(`extension host terminating: test runner requested exit with code ${code}`); + this._logService.flush(); + // wait at most 5000ms for the renderer to confirm our exit request and for the renderer socket to drain // (this is to ensure all outstanding messages reach the renderer) const exitPromise = this._mainThreadExtensionsProxy.$onExtensionHostExit(code); const drainPromise = this._extHostContext.drain(); Promise.race([Promise.all([exitPromise, drainPromise]), timeout(5000)]).then(() => { + this._logService.info(`exiting with code ${code}`); + this._logService.flush(); + this._hostUtils.exit(code); }); } @@ -644,7 +666,9 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } try { + performance.mark(`code/extHost/willResolveAuthority/${authorityPrefix}`); const result = await resolver.resolve(remoteAuthority, { resolveAttempt }); + performance.mark(`code/extHost/didResolveAuthorityOK/${authorityPrefix}`); this._disposables.add(await this._extHostTunnelService.setTunnelExtensionFunctions(resolver)); // Split merged API result into separate authority/options @@ -667,6 +691,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } }; } catch (err) { + performance.mark(`code/extHost/didResolveAuthorityError/${authorityPrefix}`); if (err instanceof RemoteAuthorityResolverError) { return { type: 'error', @@ -754,7 +779,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme protected abstract _beforeAlmostReadyToRunExtensions(): Promise; protected abstract _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined; - protected abstract _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise; + protected abstract _loadCommonJSModule(extensionId: ExtensionIdentifier | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise; public abstract $setRemoteEnvironment(env: { [key: string]: string | null }): Promise; } diff --git a/src/vs/workbench/api/common/extHostFileSystemEventService.ts b/src/vs/workbench/api/common/extHostFileSystemEventService.ts index 0503a131..6801cf0b 100644 --- a/src/vs/workbench/api/common/extHostFileSystemEventService.ts +++ b/src/vs/workbench/api/common/extHostFileSystemEventService.ts @@ -8,7 +8,7 @@ import { IRelativePattern, parse } from 'vs/base/common/glob'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import type * as vscode from 'vscode'; -import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, SourceTargetPair, IWorkspaceEditDto, MainThreadBulkEditsShape } from './extHost.protocol'; +import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, SourceTargetPair, IWorkspaceEditDto, IWillRunFileOperationParticipation } from './extHost.protocol'; import * as typeConverter from './extHostTypeConverters'; import { Disposable, WorkspaceEdit } from './extHostTypes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; @@ -122,8 +122,7 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ constructor( mainContext: IMainContext, private readonly _logService: ILogService, - private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, - private readonly _mainThreadBulkEdits: MainThreadBulkEditsShape = mainContext.getProxy(MainContext.MainThreadBulkEdits) + private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors ) { // } @@ -178,24 +177,21 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ }; } - async $onWillRunFileOperation(operation: FileOperation, files: SourceTargetPair[], undoRedoGroupId: number | undefined, timeout: number, token: CancellationToken): Promise { + async $onWillRunFileOperation(operation: FileOperation, files: SourceTargetPair[], timeout: number, token: CancellationToken): Promise { switch (operation) { case FileOperation.MOVE: - await this._fireWillEvent(this._onWillRenameFile, { files: files.map(f => ({ oldUri: URI.revive(f.source!), newUri: URI.revive(f.target) })) }, undoRedoGroupId, timeout, token); - break; + return await this._fireWillEvent(this._onWillRenameFile, { files: files.map(f => ({ oldUri: URI.revive(f.source!), newUri: URI.revive(f.target) })) }, timeout, token); case FileOperation.DELETE: - await this._fireWillEvent(this._onWillDeleteFile, { files: files.map(f => URI.revive(f.target)) }, undoRedoGroupId, timeout, token); - break; + return await this._fireWillEvent(this._onWillDeleteFile, { files: files.map(f => URI.revive(f.target)) }, timeout, token); case FileOperation.CREATE: - await this._fireWillEvent(this._onWillCreateFile, { files: files.map(f => URI.revive(f.target)) }, undoRedoGroupId, timeout, token); - break; - default: - //ignore, dont send + return await this._fireWillEvent(this._onWillCreateFile, { files: files.map(f => URI.revive(f.target)) }, timeout, token); } + return undefined; } - private async _fireWillEvent(emitter: AsyncEmitter, data: Omit, undoRedoGroupId: number | undefined, timeout: number, token: CancellationToken): Promise { + private async _fireWillEvent(emitter: AsyncEmitter, data: Omit, timeout: number, token: CancellationToken): Promise { + const extensionNames = new Set(); const edits: WorkspaceEdit[] = []; await emitter.fireAsync(data, token, async (thenable, listener) => { @@ -204,25 +200,28 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ const result = await Promise.resolve(thenable); if (result instanceof WorkspaceEdit) { edits.push(result); + extensionNames.add((>listener).extension.displayName ?? (>listener).extension.identifier.value); } if (Date.now() - now > timeout) { - this._logService.warn('SLOW file-participant', (>listener).extension?.identifier); + this._logService.warn('SLOW file-participant', (>listener).extension.identifier); } }); if (token.isCancellationRequested) { - return; + return undefined; } - if (edits.length > 0) { - // concat all WorkspaceEdits collected via waitUntil-call and apply them in one go. - const dto: IWorkspaceEditDto = { edits: [] }; - for (let edit of edits) { - let { edits } = typeConverter.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors); - dto.edits = dto.edits.concat(edits); - } - return this._mainThreadBulkEdits.$tryApplyWorkspaceEdit(dto, undoRedoGroupId); + if (edits.length === 0) { + return undefined; } + + // concat all WorkspaceEdits collected via waitUntil-call and send them over to the renderer + const dto: IWorkspaceEditDto = { edits: [] }; + for (let edit of edits) { + let { edits } = typeConverter.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors); + dto.edits = dto.edits.concat(edits); + } + return { edit: dto, extensionNames: Array.from(extensionNames) }; } } diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 49cde88e..34ca1065 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -27,11 +27,12 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio import { IURITransformer } from 'vs/base/common/uriIpc'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; -import { encodeSemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokensDto'; +import { encodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; import { IdGenerator } from 'vs/base/common/idGenerator'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; import { Cache } from './cache'; import { StopWatch } from 'vs/base/common/stopwatch'; +import { CancellationError } from 'vs/base/common/errors'; // --- adapter @@ -1062,6 +1063,20 @@ class SignatureHelpAdapter { } } +class InlineHintsAdapter { + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.InlineHintsProvider, + ) { } + + provideInlineHints(resource: URI, range: IRange, token: CancellationToken): Promise { + const doc = this._documents.getDocument(resource); + return asPromise(() => this._provider.provideInlineHints(doc, typeConvert.Range.to(range), token)).then(value => { + return value ? { hints: value.map(typeConvert.InlineHint.from) } : undefined; + }); + } +} + class LinkProviderAdapter { private _cache = new Cache('DocumentLink'); @@ -1320,7 +1335,7 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter | TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter | SelectionRangeAdapter | CallHierarchyAdapter | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter | EvaluatableExpressionAdapter - | LinkedEditingRangeAdapter; + | LinkedEditingRangeAdapter | InlineHintsAdapter; class AdapterData { constructor( @@ -1403,7 +1418,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return ExtHostLanguageFeatures._handlePool++; } - private _withAdapter(handle: number, ctor: { new(...args: any[]): A; }, callback: (adapter: A, extension: IExtensionDescription | undefined) => Promise, fallbackValue: R): Promise { + private _withAdapter(handle: number, ctor: { new(...args: any[]): A; }, callback: (adapter: A, extension: IExtensionDescription | undefined) => Promise, fallbackValue: R, allowCancellationError: boolean = false): Promise { const data = this._adapter.get(handle); if (!data) { return Promise.resolve(fallbackValue); @@ -1421,8 +1436,11 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF Promise.resolve(p).then( () => this._logService.trace(`[${extension.identifier.value}] provider DONE after ${Date.now() - t1}ms`), err => { - this._logService.error(`[${extension.identifier.value}] provider FAILED`); - this._logService.error(err); + const isExpectedError = allowCancellationError && (err instanceof CancellationError); + if (!isExpectedError) { + this._logService.error(`[${extension.identifier.value}] provider FAILED`); + this._logService.error(err); + } } ); } @@ -1711,7 +1729,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF } $provideDocumentSemanticTokens(handle: number, resource: UriComponents, previousResultId: number, token: CancellationToken): Promise { - return this._withAdapter(handle, DocumentSemanticTokensAdapter, adapter => adapter.provideDocumentSemanticTokens(URI.revive(resource), previousResultId, token), null); + return this._withAdapter(handle, DocumentSemanticTokensAdapter, adapter => adapter.provideDocumentSemanticTokens(URI.revive(resource), previousResultId, token), null, true); } $releaseDocumentSemanticTokens(handle: number, semanticColoringResultId: number): void { @@ -1725,7 +1743,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF } $provideDocumentRangeSemanticTokens(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise { - return this._withAdapter(handle, DocumentRangeSemanticTokensAdapter, adapter => adapter.provideDocumentRangeSemanticTokens(URI.revive(resource), range, token), null); + return this._withAdapter(handle, DocumentRangeSemanticTokensAdapter, adapter => adapter.provideDocumentRangeSemanticTokens(URI.revive(resource), range, token), null, true); } //#endregion @@ -1770,6 +1788,27 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF this._withAdapter(handle, SignatureHelpAdapter, adapter => adapter.releaseSignatureHelp(id), undefined); } + // --- inline hints + + registerInlineHintsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.InlineHintsProvider): vscode.Disposable { + + const eventHandle = typeof provider.onDidChangeInlineHints === 'function' ? this._nextHandle() : undefined; + const handle = this._addNewAdapter(new InlineHintsAdapter(this._documents, provider), extension); + + this._proxy.$registerInlineHintsProvider(handle, this._transformDocumentSelector(selector), eventHandle); + let result = this._createDisposable(handle); + + if (eventHandle !== undefined) { + const subscription = provider.onDidChangeInlineHints!(_ => this._proxy.$emitInlineHintsEvent(eventHandle)); + result = Disposable.from(result, subscription); + } + return result; + } + + $provideInlineHints(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise { + return this._withAdapter(handle, InlineHintsAdapter, adapter => adapter.provideInlineHints(URI.revive(resource), range, token), undefined); + } + // --- links registerDocumentLinkProvider(extension: IExtensionDescription | undefined, selector: vscode.DocumentSelector, provider: vscode.DocumentLinkProvider): vscode.Disposable { @@ -1882,7 +1921,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return { beforeText: ExtHostLanguageFeatures._serializeRegExp(onEnterRule.beforeText), afterText: onEnterRule.afterText ? ExtHostLanguageFeatures._serializeRegExp(onEnterRule.afterText) : undefined, - oneLineAboveText: onEnterRule.oneLineAboveText ? ExtHostLanguageFeatures._serializeRegExp(onEnterRule.oneLineAboveText) : undefined, + previousLineText: onEnterRule.previousLineText ? ExtHostLanguageFeatures._serializeRegExp(onEnterRule.previousLineText) : undefined, action: onEnterRule.action }; } diff --git a/src/vs/workbench/api/common/extHostMessageService.ts b/src/vs/workbench/api/common/extHostMessageService.ts index ff75e2f3..847a27a2 100644 --- a/src/vs/workbench/api/common/extHostMessageService.ts +++ b/src/vs/workbench/api/common/extHostMessageService.ts @@ -8,6 +8,7 @@ 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'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; function isMessageItem(item: any): item is vscode.MessageItem { return item && item.title; @@ -37,9 +38,14 @@ export class ExtHostMessageService { items = [optionsOrFirstItem, ...rest]; } else { options.modal = optionsOrFirstItem && optionsOrFirstItem.modal; + options.useCustom = optionsOrFirstItem && optionsOrFirstItem.useCustom; items = rest; } + if (options.useCustom) { + checkProposedApiEnabled(extension); + } + const commands: { title: string; isCloseAffordance: boolean; handle: number; }[] = []; for (let handle = 0; handle < items.length; handle++) { diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index bb136d1c..298548f2 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -78,8 +78,8 @@ export interface ExtHostNotebookOutputRenderingHandler { } export class ExtHostNotebookKernelProviderAdapter extends Disposable { - private _kernelToId = new Map(); - private _idToKernel = new Map(); + private _kernelToFriendlyId = new Map(); + private _friendlyIdToKernel = new Map(); constructor( private readonly _proxy: MainThreadNotebookShape, private readonly _handle: number, @@ -101,24 +101,25 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { const newMap = new Map(); let kernel_unique_pool = 0; - const kernelIdCache = new Set(); + const kernelFriendlyIdCache = new Set(); const transformedData: INotebookKernelInfoDto2[] = data.map(kernel => { - let id = this._kernelToId.get(kernel); - if (id === undefined) { - if (kernel.id && kernelIdCache.has(kernel.id)) { - id = `${this._extension.identifier.value}_${kernel.id}_${kernel_unique_pool++}`; + let friendlyId = this._kernelToFriendlyId.get(kernel); + if (friendlyId === undefined) { + if (kernel.id && kernelFriendlyIdCache.has(kernel.id)) { + friendlyId = `${this._extension.identifier.value}_${kernel.id}_${kernel_unique_pool++}`; } else { - id = `${this._extension.identifier.value}_${kernel.id || UUID.generateUuid()}`; + friendlyId = `${this._extension.identifier.value}_${kernel.id || UUID.generateUuid()}`; } - this._kernelToId.set(kernel, id); + this._kernelToFriendlyId.set(kernel, friendlyId); } - newMap.set(kernel, id); + newMap.set(kernel, friendlyId); return { - id, + id: kernel.id, + friendlyId: friendlyId, label: kernel.label, extension: this._extension.identifier, extensionLocation: this._extension.extensionLocation, @@ -129,22 +130,22 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { }; }); - this._kernelToId = newMap; + this._kernelToFriendlyId = newMap; - this._idToKernel.clear(); - this._kernelToId.forEach((value, key) => { - this._idToKernel.set(value, key); + this._friendlyIdToKernel.clear(); + this._kernelToFriendlyId.forEach((value, key) => { + this._friendlyIdToKernel.set(value, key); }); return transformedData; } - getKernel(kernelId: string) { - return this._idToKernel.get(kernelId); + getKernelByFriendlyId(kernelId: string) { + return this._friendlyIdToKernel.get(kernelId); } async resolveNotebook(kernelId: string, document: ExtHostNotebookDocument, webview: vscode.NotebookCommunication, token: CancellationToken) { - const kernel = this._idToKernel.get(kernelId); + const kernel = this._friendlyIdToKernel.get(kernelId); if (kernel && this._provider.resolveKernel) { return this._provider.resolveKernel(kernel, document.notebookDocument, webview, token); @@ -152,7 +153,7 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { } async executeNotebook(kernelId: string, document: ExtHostNotebookDocument, cell: ExtHostCell | undefined) { - const kernel = this._idToKernel.get(kernelId); + const kernel = this._friendlyIdToKernel.get(kernelId); if (!kernel) { return; @@ -166,7 +167,7 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { } async cancelNotebook(kernelId: string, document: ExtHostNotebookDocument, cell: ExtHostCell | undefined) { - const kernel = this._idToKernel.get(kernelId); + const kernel = this._friendlyIdToKernel.get(kernelId); if (!kernel) { return; @@ -580,10 +581,10 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._outputDisplayOrder = displayOrder; } - $acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelId: string | undefined; }) { + $acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelFriendlyId: string | undefined; }) { if (event.providerHandle !== undefined) { this._withAdapter(event.providerHandle, event.uri, async (adapter, document) => { - const kernel = event.kernelId ? adapter.getKernel(event.kernelId) : undefined; + const kernel = event.kernelFriendlyId ? adapter.getKernelByFriendlyId(event.kernelFriendlyId) : undefined; this._editors.forEach(editor => { if (editor.editor.notebookData === document) { editor.editor._acceptKernel(kernel); diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index e67332a4..85f9083b 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -113,14 +113,17 @@ export class ExtHostCell extends Disposable { get cell(): vscode.NotebookCell { if (!this._cell) { const that = this; - const document = this._extHostDocument.getDocument(this.uri)!.document; + const data = this._extHostDocument.getDocument(this.uri); + if (!data) { + throw new Error(`MISSING extHostDocument for notebook cell: ${this.uri}`); + } this._cell = Object.freeze({ get index() { return that._notebook.getCellIndex(that); }, notebook: that._notebook.notebookDocument, uri: that.uri, cellKind: this._cellData.cellKind, - document, - get language() { return document.languageId; }, + document: data.document, + get language() { return data!.document.languageId; }, get outputs() { return that._outputs; }, set outputs(value) { that._updateOutputs(value); }, get metadata() { return that._metadata; }, diff --git a/src/vs/workbench/api/common/extHostRequireInterceptor.ts b/src/vs/workbench/api/common/extHostRequireInterceptor.ts index fd9ae5f2..f59d41ce 100644 --- a/src/vs/workbench/api/common/extHostRequireInterceptor.ts +++ b/src/vs/workbench/api/common/extHostRequireInterceptor.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as performance from 'vs/base/common/performance'; import { TernarySearchTree } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; import { MainThreadTelemetryShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; @@ -52,7 +53,9 @@ export abstract class RequireInterceptor { this._installInterceptor(); + performance.mark('code/extHost/willWaitForConfig'); const configProvider = await this._extHostConfiguration.getConfigProvider(); + performance.mark('code/extHost/didWaitForConfig'); const extensionPaths = await this._extHostExtensionService.getExtensionPathIndex(); this.register(new VSCodeNodeModuleFactory(this._apiFactory, extensionPaths, this._extensionRegistry, configProvider, this._logService)); diff --git a/src/vs/workbench/api/common/extHostSecrets.ts b/src/vs/workbench/api/common/extHostSecrets.ts new file mode 100644 index 00000000..6d973d31 --- /dev/null +++ b/src/vs/workbench/api/common/extHostSecrets.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; + +import { ExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { Emitter, Event } from 'vs/base/common/event'; + +export class ExtensionSecrets implements vscode.SecretStorage { + + protected readonly _id: string; + readonly #secretState: ExtHostSecretState; + + private _onDidChange = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + + + constructor(extensionDescription: IExtensionDescription, secretState: ExtHostSecretState) { + this._id = ExtensionIdentifier.toKey(extensionDescription.identifier); + this.#secretState = secretState; + + this.#secretState.onDidChangePassword(e => { + if (e.extensionId === this._id) { + this._onDidChange.fire({ key: e.key }); + } + }); + } + + get(key: string): Promise { + return this.#secretState.get(this._id, key); + } + + store(key: string, value: string): Promise { + return this.#secretState.store(this._id, key, value); + } + + delete(key: string): Promise { + return this.#secretState.delete(this._id, key); + } +} diff --git a/src/vs/workbench/api/common/extHostStatusBar.ts b/src/vs/workbench/api/common/extHostStatusBar.ts index a89e9fe3..877d56e7 100644 --- a/src/vs/workbench/api/common/extHostStatusBar.ts +++ b/src/vs/workbench/api/common/extHostStatusBar.ts @@ -10,8 +10,6 @@ import { MainContext, MainThreadStatusBarShape, IMainContext, ICommandDto } from import { localize } from 'vs/nls'; import { CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; export class ExtHostStatusBarEntry implements vscode.StatusBarItem { private static ID_GEN = 0; @@ -48,9 +46,8 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { private _proxy: MainThreadStatusBarShape; private _commands: CommandsConverter; private _accessibilityInformation?: vscode.AccessibilityInformation; - private _extension?: IExtensionDescription; - constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, id: string, name: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation, extension?: IExtensionDescription) { + constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, id: string, name: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation) { this._id = ExtHostStatusBarEntry.ID_GEN++; this._proxy = proxy; this._commands = commands; @@ -59,7 +56,6 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { this._alignment = alignment; this._priority = priority; this._accessibilityInformation = accessibilityInformation; - this._extension = extension; } public get id(): number { @@ -87,10 +83,6 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { } public get backgroundColor(): ThemeColor | undefined { - if (this._extension) { - checkProposedApiEnabled(this._extension); - } - return this._backgroundColor; } @@ -118,10 +110,6 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { } public set backgroundColor(color: ThemeColor | undefined) { - if (this._extension) { - checkProposedApiEnabled(this._extension); - } - if (color && !ExtHostStatusBarEntry.ALLOWED_BACKGROUND_COLORS.has(color.id)) { color = undefined; } @@ -248,8 +236,8 @@ export class ExtHostStatusBar { this._statusMessage = new StatusBarMessage(this); } - createStatusBarEntry(id: string, name: string, alignment?: ExtHostStatusBarAlignment, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation, extension?: IExtensionDescription): vscode.StatusBarItem { - return new ExtHostStatusBarEntry(this._proxy, this._commands, id, name, alignment, priority, accessibilityInformation, extension); + createStatusBarEntry(id: string, name: string, alignment?: ExtHostStatusBarAlignment, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation): vscode.StatusBarItem { + return new ExtHostStatusBarEntry(this._proxy, this._commands, id, name, alignment, priority, accessibilityInformation); } setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): Disposable { diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 1cccd958..f684dbb8 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -5,12 +5,11 @@ import type * as vscode from 'vscode'; import { Event, Emitter } from 'vs/base/common/event'; -import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IShellLaunchConfigDto, IShellDefinitionDto, IShellAndArgsDto, ITerminalDimensionsDto, ITerminalLinkDto } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IShellLaunchConfigDto, IShellDefinitionDto, IShellAndArgsDto, ITerminalDimensionsDto, ITerminalLinkDto, TerminalIdentifier } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { ITerminalChildProcess, EXT_HOST_CREATION_DELAY, ITerminalLaunchError, ITerminalDimensionsOverride } from 'vs/workbench/contrib/terminal/common/terminal'; -import { timeout } from 'vs/base/common/async'; +import { ITerminalChildProcess, ITerminalLaunchError, ITerminalDimensionsOverride } from 'vs/workbench/contrib/terminal/common/terminal'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering'; import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; @@ -21,6 +20,7 @@ import { localize } from 'vs/nls'; import { NotSupportedError } from 'vs/base/common/errors'; import { serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { generateUuid } from 'vs/base/common/uuid'; export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, IDisposable { @@ -47,63 +47,8 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, ID export const IExtHostTerminalService = createDecorator('IExtHostTerminalService'); -export class BaseExtHostTerminal { - public _id: number | undefined; - protected _idPromise: Promise; - private _idPromiseComplete: ((value: number) => any) | undefined; +export class ExtHostTerminal implements vscode.Terminal { private _disposed: boolean = false; - private _queuedRequests: ApiRequest[] = []; - - constructor( - protected _proxy: MainThreadTerminalServiceShape, - id?: number - ) { - this._idPromise = new Promise(c => { - if (id !== undefined) { - this._id = id; - c(id); - } else { - this._idPromiseComplete = c; - } - }); - } - - public dispose(): void { - if (!this._disposed) { - this._disposed = true; - this._queueApiRequest(this._proxy.$dispose, []); - } - } - - protected _checkDisposed() { - if (this._disposed) { - throw new Error('Terminal has already been disposed'); - } - } - - protected _queueApiRequest(callback: (...args: any[]) => void, args: any[]): void { - const request: ApiRequest = new ApiRequest(callback, args); - if (!this._id) { - this._queuedRequests.push(request); - return; - } - request.run(this._proxy, this._id); - } - - public _runQueuedRequests(id: number): void { - this._id = id; - if (this._idPromiseComplete) { - this._idPromiseComplete(id); - this._idPromiseComplete = undefined; - } - this._queuedRequests.forEach((r) => { - r.run(this._proxy, id); - }); - this._queuedRequests.length = 0; - } -} - -export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Terminal { private _pidPromise: Promise; private _cols: number | undefined; private _pidPromiseComplete: ((value: number | undefined) => any) | undefined; @@ -113,12 +58,11 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi public isOpen: boolean = false; constructor( - proxy: MainThreadTerminalServiceShape, + private _proxy: MainThreadTerminalServiceShape, + public _id: TerminalIdentifier, private readonly _creationOptions: vscode.TerminalOptions | vscode.ExtensionTerminalOptions, private _name?: string, - id?: number ) { - super(proxy, id); this._creationOptions = Object.freeze(this._creationOptions); this._pidPromise = new Promise(c => this._pidPromiseComplete = c); } @@ -133,16 +77,35 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi hideFromUser?: boolean, isFeatureTerminal?: boolean ): Promise { - const result = await this._proxy.$createTerminal({ name: this._name, shellPath, shellArgs, cwd, env, waitOnExit, strictEnv, hideFromUser, isFeatureTerminal }); - this._name = result.name; - this._runQueuedRequests(result.id); + if (typeof this._id !== 'string') { + throw new Error('Terminal has already been created'); + } + await this._proxy.$createTerminal(this._id, { name: this._name, shellPath, shellArgs, cwd, env, waitOnExit, strictEnv, hideFromUser, isFeatureTerminal }); } public async createExtensionTerminal(): Promise { - const result = await this._proxy.$createTerminal({ name: this._name, isExtensionTerminal: true }); - this._name = result.name; - this._runQueuedRequests(result.id); - return result.id; + if (typeof this._id !== 'string') { + throw new Error('Terminal has already been created'); + } + await this._proxy.$createTerminal(this._id, { name: this._name, isExtensionTerminal: true }); + // At this point, the id has been set via `$acceptTerminalOpened` + if (typeof this._id === 'string') { + throw new Error('Terminal creation failed'); + } + return this._id; + } + + public dispose(): void { + if (!this._disposed) { + this._disposed = true; + this._proxy.$dispose(this._id); + } + } + + private _checkDisposed() { + if (this._disposed) { + throw new Error('Terminal has already been disposed'); + } } public get name(): string { @@ -194,17 +157,17 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi public sendText(text: string, addNewLine: boolean = true): void { this._checkDisposed(); - this._queueApiRequest(this._proxy.$sendText, [text, addNewLine]); + this._proxy.$sendText(this._id, text, addNewLine); } public show(preserveFocus: boolean): void { this._checkDisposed(); - this._queueApiRequest(this._proxy.$show, [preserveFocus]); + this._proxy.$show(this._id, preserveFocus); } public hide(): void { this._checkDisposed(); - this._queueApiRequest(this._proxy.$hide, []); + this._proxy.$hide(this._id); } public _setProcessId(processId: number | undefined): void { @@ -223,20 +186,6 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi } } -class ApiRequest { - private _callback: (...args: any[]) => void; - private _args: any[]; - - constructor(callback: (...args: any[]) => void, args: any[]) { - this._callback = callback; - this._args = args; - } - - public run(proxy: MainThreadTerminalServiceShape, id: number) { - this._callback.apply(proxy, [id].concat(this._args)); - } -} - export class ExtHostPseudoterminal implements ITerminalChildProcess { private readonly _onProcessData = new Emitter(); public readonly onProcessData: Event = this._onProcessData.event; @@ -271,6 +220,11 @@ export class ExtHostPseudoterminal implements ITerminalChildProcess { } } + acknowledgeDataEvent(charCount: number): void { + // No-op, flow control is not supported in extension owned terminals. If this is ever + // implemented it will need new pause and resume VS Code APIs. + } + getInitialCwd(): Promise { return Promise.resolve(''); } @@ -296,6 +250,11 @@ export class ExtHostPseudoterminal implements ITerminalChildProcess { } this._pty.open(initialDimensions ? initialDimensions : undefined); + + if (this._pty.setDimensions && initialDimensions) { + this._pty.setDimensions(initialDimensions); + } + this._onProcessReady.fire({ pid: -1, cwd: '' }); } } @@ -370,7 +329,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public abstract $acceptWorkspacePermissionsChanged(isAllowed: boolean): void; public createExtensionTerminal(options: vscode.ExtensionTerminalOptions): vscode.Terminal { - const terminal = new ExtHostTerminal(this._proxy, options, options.name); + const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name); const p = new ExtHostPseudoterminal(options.pty); terminal.createExtensionTerminal().then(id => { const disposable = this._setupExtHostProcessListeners(id, p); @@ -381,7 +340,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } public attachPtyToTerminal(id: number, pty: vscode.Pseudoterminal): void { - const terminal = this._getTerminalByIdEventually(id); + const terminal = this._getTerminalById(id); if (!terminal) { throw new Error(`Cannot resolve terminal with id ${id} for virtual process`); } @@ -399,7 +358,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } return; } - const terminal = await this._getTerminalByIdEventually(id); + const terminal = this._getTerminalById(id); if (terminal) { this._activeTerminal = terminal; if (original !== this._activeTerminal) { @@ -409,14 +368,14 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } public async $acceptTerminalProcessData(id: number, data: string): Promise { - const terminal = await this._getTerminalByIdEventually(id); + const terminal = this._getTerminalById(id); if (terminal) { this._onDidWriteTerminalData.fire({ terminal, data }); } } public async $acceptTerminalDimensions(id: number, cols: number, rows: number): Promise { - const terminal = await this._getTerminalByIdEventually(id); + const terminal = this._getTerminalById(id); if (terminal) { if (terminal.setDimensions(cols, rows)) { this._onDidChangeTerminalDimensions.fire({ @@ -428,23 +387,19 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } public async $acceptTerminalMaximumDimensions(id: number, cols: number, rows: number): Promise { - await this._getTerminalByIdEventually(id); - // Extension pty terminal only - when virtual process resize fires it means that the // terminal's maximum dimensions changed this._terminalProcesses.get(id)?.resize(cols, rows); } public async $acceptTerminalTitleChange(id: number, name: string): Promise { - await this._getTerminalByIdEventually(id); - const extHostTerminal = this._getTerminalObjectById(this.terminals, id); - if (extHostTerminal) { - extHostTerminal.name = name; + const terminal = this._getTerminalById(id); + if (terminal) { + terminal.name = name; } } public async $acceptTerminalClosed(id: number, exitCode: number | undefined): Promise { - await this._getTerminalByIdEventually(id); const index = this._getTerminalObjectIndexById(this.terminals, id); if (index !== null) { const terminal = this._terminals.splice(index, 1)[0]; @@ -453,13 +408,17 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } } - public $acceptTerminalOpened(id: number, name: string, shellLaunchConfigDto: IShellLaunchConfigDto): void { - const index = this._getTerminalObjectIndexById(this._terminals, id); - if (index !== null) { - // The terminal has already been created (via createTerminal*), only fire the event - this._onDidOpenTerminal.fire(this.terminals[index]); - this.terminals[index].isOpen = true; - return; + public $acceptTerminalOpened(id: number, extHostTerminalId: string | undefined, name: string, shellLaunchConfigDto: IShellLaunchConfigDto): void { + if (extHostTerminalId) { + // Resolve with the renderer generated id + const index = this._getTerminalObjectIndexById(this._terminals, extHostTerminalId); + if (index !== null) { + // The terminal has already been created (via createTerminal*), only fire the event + this.terminals[index]._id = id; + this._onDidOpenTerminal.fire(this.terminals[index]); + this.terminals[index].isOpen = true; + return; + } } const creationOptions: vscode.TerminalOptions = { @@ -470,14 +429,14 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I env: shellLaunchConfigDto.env, hideFromUser: shellLaunchConfigDto.hideFromUser }; - const terminal = new ExtHostTerminal(this._proxy, creationOptions, name, id); + const terminal = new ExtHostTerminal(this._proxy, id, creationOptions, name); this._terminals.push(terminal); this._onDidOpenTerminal.fire(terminal); terminal.isOpen = true; } public async $acceptTerminalProcessId(id: number, processId: number): Promise { - const terminal = await this._getTerminalByIdEventually(id); + const terminal = this._getTerminalById(id); if (terminal) { terminal._setProcessId(processId); } @@ -486,7 +445,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public async $startExtensionTerminal(id: number, initialDimensions: ITerminalDimensionsDto | undefined): Promise { // Make sure the ExtHostTerminal exists so onDidOpenTerminal has fired before we call // Pseudoterminal.start - const terminal = await this._getTerminalByIdEventually(id); + const terminal = this._getTerminalById(id); if (!terminal) { return { message: localize('launchFail.idMissingOnExtHost', "Could not find the terminal with id {0} on the extension host", id) }; } @@ -539,6 +498,10 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I return disposables; } + public $acceptProcessAckDataEvent(id: number, charCount: number): void { + this._terminalProcesses.get(id)?.acknowledgeDataEvent(charCount); + } + public $acceptProcessInput(id: number, data: string): void { this._terminalProcesses.get(id)?.input(data); } @@ -670,32 +633,6 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I this._proxy.$sendProcessExit(id, exitCode); } - // TODO: This could be improved by using a single promise and resolve it when the terminal is ready - private _getTerminalByIdEventually(id: number, retries: number = 5): Promise { - if (!this._getTerminalPromises[id]) { - this._getTerminalPromises[id] = this._createGetTerminalPromise(id, retries); - } - return this._getTerminalPromises[id]; - } - - private _createGetTerminalPromise(id: number, retries: number = 5): Promise { - return new Promise(c => { - if (retries === 0) { - c(undefined); - return; - } - - const terminal = this._getTerminalById(id); - if (terminal) { - c(terminal); - } else { - // This should only be needed immediately after createTerminalRenderer is called as - // the ExtHostTerminal has not yet been iniitalized - timeout(EXT_HOST_CREATION_DELAY * 2).then(() => c(this._createGetTerminalPromise(id, retries - 1))); - } - }); - } - private _getTerminalById(id: number): ExtHostTerminal | null { return this._getTerminalObjectById(this._terminals, id); } @@ -705,7 +642,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I return index !== null ? array[index] : null; } - private _getTerminalObjectIndexById(array: T[], id: number): number | null { + private _getTerminalObjectIndexById(array: T[], id: TerminalIdentifier): number | null { let index: number | null = null; array.some((item, i) => { const thisId = item._id; diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 02fc32cb..fb308456 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -6,7 +6,6 @@ import { mapFind } from 'vs/base/common/arrays'; import { disposableTimeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { throttle } from 'vs/base/common/decorators'; import { Emitter } from 'vs/base/common/event'; import { once } from 'vs/base/common/functional'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; @@ -14,25 +13,35 @@ import { isDefined } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { ExtHostTestingResource, ExtHostTestingShape, MainContext, MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { TestItem } from 'vs/workbench/api/common/extHostTypeConverters'; -import { Disposable, RequiredTestItem } from 'vs/workbench/api/common/extHostTypes'; +import { Disposable } from 'vs/workbench/api/common/extHostTypes'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { AbstractIncrementalTestCollection, EMPTY_TEST_RESULT, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, RunTestForProviderRequest, RunTestsResult, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { OwnedTestCollection, SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection'; +import { AbstractIncrementalTestCollection, EMPTY_TEST_RESULT, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, InternalTestItemWithChildren, InternalTestResults, RunTestForProviderRequest, RunTestsResult, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import type * as vscode from 'vscode'; const getTestSubscriptionKey = (resource: ExtHostTestingResource, uri: URI) => `${resource}:${uri.toString()}`; export class ExtHostTesting implements ExtHostTestingShape { + private readonly resultsChangedEmitter = new Emitter(); private readonly providers = new Map(); private readonly proxy: MainThreadTestingShape; private readonly ownedTests = new OwnedTestCollection(); - private readonly testSubscriptions = new Map(); + private readonly testSubscriptions = new Map void; + }>(); private workspaceObservers: WorkspaceFolderTestObserverFactory; private textDocumentObservers: TextDocumentTestObserverFactory; + public onLastResultsChanged = this.resultsChangedEmitter.event; + public lastResults?: vscode.TestResults; + constructor(@IExtHostRpcService rpc: IExtHostRpcService, @IExtHostDocumentsAndEditors private readonly documents: IExtHostDocumentsAndEditors, @IExtHostWorkspace private readonly workspace: IExtHostWorkspace) { this.proxy = rpc.getProxy(MainContext.MainThreadTesting); this.workspaceObservers = new WorkspaceFolderTestObserverFactory(this.proxy); @@ -47,6 +56,14 @@ export class ExtHostTesting implements ExtHostTestingShape { this.providers.set(providerId, provider); this.proxy.$registerTestProvider(providerId); + // give the ext a moment to register things rather than synchronously invoking within activate() + const toSubscribe = [...this.testSubscriptions.keys()]; + setTimeout(() => { + for (const subscription of toSubscribe) { + this.testSubscriptions.get(subscription)?.subscribeFn(providerId, provider); + } + }, 0); + return new Disposable(() => { this.providers.delete(providerId); this.proxy.$unregisterTestProvider(providerId); @@ -70,7 +87,7 @@ export class ExtHostTesting implements ExtHostTestingShape { /** * Implements vscode.test.runTests */ - public async runTests(req: vscode.TestRunOptions) { + public async runTests(req: vscode.TestRunOptions, token = CancellationToken.None) { await this.proxy.$runTests({ tests: req.tests // Find workspace items first, then owned tests, then document tests. @@ -82,14 +99,27 @@ export class ExtHostTesting implements ExtHostTestingShape { .filter(isDefined) .map(item => ({ providerId: item.providerId, testId: item.id })), debug: req.debug - }); + }, token); + } + + + /** + * Updates test results shown to extensions. + * @override + */ + public $publishTestResults(results: InternalTestResults): void { + const convert = (item: InternalTestItemWithChildren): vscode.RequiredTestItem => + ({ ...TestItem.toShallow(item.item), children: item.children.map(convert) }); + + this.lastResults = { tests: results.tests.map(convert) }; + this.resultsChangedEmitter.fire(); } /** * Handles a request to read tests for a file, or workspace. * @override */ - public $subscribeToTests(resource: ExtHostTestingResource, uriComponents: UriComponents) { + public async $subscribeToTests(resource: ExtHostTestingResource, uriComponents: UriComponents) { const uri = URI.revive(uriComponents); const subscriptionKey = getTestSubscriptionKey(resource, uri); if (this.testSubscriptions.has(subscriptionKey)) { @@ -98,12 +128,29 @@ export class ExtHostTesting implements ExtHostTestingShape { let method: undefined | ((p: vscode.TestProvider) => vscode.TestHierarchy | undefined); if (resource === ExtHostTestingResource.TextDocument) { - const document = this.documents.getDocument(uri); + let document = this.documents.getDocument(uri); + + // we can ask to subscribe to tests before the documents are populated in + // the extension host. Try to wait. + if (!document) { + const store = new DisposableStore(); + document = await new Promise(resolve => { + store.add(disposableTimeout(() => resolve(undefined), 5000)); + store.add(this.documents.onDidAddDocuments(e => { + const data = e.find(data => data.document.uri.toString() === uri.toString()); + if (data) { resolve(data); } + })); + }).finally(() => store.dispose()); + } + if (document) { - method = p => p.createDocumentTestHierarchy?.(document.document); + const folder = await this.workspace.getWorkspaceFolder2(uri, false); + method = p => p.createDocumentTestHierarchy + ? p.createDocumentTestHierarchy(document!.document) + : this.createDefaultDocumentTestHierarchy(p, document!.document, folder); } } else { - const folder = this.workspace.getWorkspaceFolder(uri, false); + const folder = await this.workspace.getWorkspaceFolder2(uri, false); if (folder) { method = p => p.createWorkspaceTestHierarchy?.(folder); } @@ -113,24 +160,34 @@ export class ExtHostTesting implements ExtHostTestingShape { return; } - const disposable = new DisposableStore(); - const collection = disposable.add(this.ownedTests.createForHierarchy(diff => this.proxy.$publishDiff(resource, uriComponents, diff))); - for (const [id, provider] of this.providers) { + const subscribeFn = (id: string, provider: vscode.TestProvider) => { try { - const hierarchy = method(provider); + const hierarchy = method!(provider); if (!hierarchy) { - continue; + return; } + collection.pushDiff([TestDiffOpType.DeltaDiscoverComplete, 1]); disposable.add(hierarchy); collection.addRoot(hierarchy.root, id); + Promise.resolve(hierarchy.discoveredInitialTests).then(() => collection.pushDiff([TestDiffOpType.DeltaDiscoverComplete, -1])); hierarchy.onDidChangeTest(e => collection.onItemChange(e, id)); } catch (e) { console.error(e); } + }; + + const disposable = new DisposableStore(); + const collection = disposable.add(this.ownedTests.createForHierarchy(diff => this.proxy.$publishDiff(resource, uriComponents, diff))); + for (const [id, provider] of this.providers) { + subscribeFn(id, provider); } - this.testSubscriptions.set(subscriptionKey, { store: disposable, collection }); + // note: we don't increment the root count initially -- this is done by the + // main thread, incrementing once per extension host. We just push the + // diff to signal that roots have been discovered. + collection.pushDiff([TestDiffOpType.DeltaRootsComplete, -1]); + this.testSubscriptions.set(subscriptionKey, { store: disposable, collection, subscribeFn }); } /** @@ -162,217 +219,193 @@ export class ExtHostTesting implements ExtHostTestingShape { * providers to be run. * @override */ - public async $runTestsForProvider(req: RunTestForProviderRequest): Promise { + public async $runTestsForProvider(req: RunTestForProviderRequest, cancellation: CancellationToken): Promise { const provider = this.providers.get(req.providerId); if (!provider || !provider.runTests) { return EMPTY_TEST_RESULT; } - const tests = req.ids.map(id => this.ownedTests.getTestById(id)?.actual).filter(isDefined); + const tests = req.ids.map(id => this.ownedTests.getTestById(id)?.actual) + .filter(isDefined) + // Only send the actual TestItem's to the user to run. + .map(t => t instanceof TestItemFilteredWrapper ? t.actual : t); if (!tests.length) { return EMPTY_TEST_RESULT; } - await provider.runTests({ tests, debug: req.debug }, CancellationToken.None); - return EMPTY_TEST_RESULT; - } -} - -const keyMap: { [K in keyof Omit]: null } = { - label: null, - location: null, - state: null, - debuggable: null, - description: null, - runnable: null -}; - -const simpleProps = Object.keys(keyMap) as ReadonlyArray; - -const itemEqualityComparator = (a: vscode.TestItem) => { - const values: unknown[] = []; - for (const prop of simpleProps) { - values.push(a[prop]); - } - - return (b: vscode.TestItem) => { - for (let i = 0; i < simpleProps.length; i++) { - if (values[i] !== b[simpleProps[i]]) { - return false; + try { + await provider.runTests({ tests, debug: req.debug }, cancellation); + for (const { collection } of this.testSubscriptions.values()) { + collection.flushDiff(); // ensure all states are updated } + + return EMPTY_TEST_RESULT; + } catch (e) { + console.error(e); // so it appears to attached debuggers + throw e; + } + } + + public $lookupTest(req: TestIdWithProvider): Promise { + const owned = this.ownedTests.getTestById(req.testId); + if (!owned) { + return Promise.resolve(undefined); } - return true; - }; -}; - -/** - * @private - */ -export interface OwnedCollectionTestItem extends InternalTestItem { - actual: vscode.TestItem; - previousChildren: Set; - previousEquals: (v: vscode.TestItem) => boolean; -} - -/** - * @private - */ -export class OwnedTestCollection { - protected readonly testIdToInternal = new Map(); - - /** - * Gets test information by ID, if it was defined and still exists in this - * extension host. - */ - public getTestById(id: string) { - return this.testIdToInternal.get(id); + const { actual, previousChildren, previousEquals, ...item } = owned; + return Promise.resolve(item); } - /** - * Creates a new test collection for a specific hierarchy for a workspace - * or document observation. - */ - public createForHierarchy(publishDiff: (diff: TestsDiff) => void = () => undefined) { - return new SingleUseTestCollection(this.testIdToInternal, publishDiff); - } -} - -/** - * Maintains tests created and registered for a single set of hierarchies - * for a workspace or document. - * @private - */ -export class SingleUseTestCollection implements IDisposable { - protected readonly testItemToInternal = new Map(); - protected diff: TestsDiff = []; - private disposed = false; - - constructor(private readonly testIdToInternal: Map, private readonly publishDiff: (diff: TestsDiff) => void) { } - - /** - * Adds a new root node to the collection. - */ - public addRoot(item: vscode.TestItem, providerId: string) { - this.addItem(item, providerId, null); - this.throttleSendDiff(); - } - - /** - * Gets test information by its reference, if it was defined and still exists - * in this extension host. - */ - public getTestByReference(item: vscode.TestItem) { - return this.testItemToInternal.get(item); - } - - /** - * Should be called when an item change is fired on the test provider. - */ - public onItemChange(item: vscode.TestItem, providerId: string) { - const existing = this.testItemToInternal.get(item); - if (!existing) { - if (!this.disposed) { - console.warn(`Received a TestProvider.onDidChangeTest for a test that wasn't seen before as a child.`); - } + private createDefaultDocumentTestHierarchy(provider: vscode.TestProvider, document: vscode.TextDocument, folder: vscode.WorkspaceFolder | undefined): vscode.TestHierarchy | undefined { + if (!folder) { return; } - this.addItem(item, providerId, existing.parent); - this.throttleSendDiff(); - } - - /** - * Gets a diff of all changes that have been made, and clears the diff queue. - */ - public collectDiff() { - const diff = this.diff; - this.diff = []; - return diff; - } - - public dispose() { - for (const item of this.testItemToInternal.values()) { - this.testIdToInternal.delete(item.id); + const workspaceHierarchy = provider.createWorkspaceTestHierarchy?.(folder); + if (!workspaceHierarchy) { + return; } - this.testIdToInternal.clear(); - this.diff = []; - this.disposed = true; - } + const onDidChangeTest = new Emitter(); + workspaceHierarchy.onDidChangeTest(node => { + const wrapper = TestItemFilteredWrapper.getWrapperForTestItem(node, document); + const previouslySeen = wrapper.hasNodeMatchingFilter; - protected getId(): string { - return generateUuid(); - } + if (previouslySeen) { + // reset cache and get whether you can currently see the TestItem. + wrapper.reset(); + const currentlySeen = wrapper.hasNodeMatchingFilter; - private addItem(actual: vscode.TestItem, providerId: string, parent: string | null) { - let internal = this.testItemToInternal.get(actual); - if (!internal) { - internal = { - actual, - id: this.getId(), - parent, - item: TestItem.from(actual), - providerId, - previousChildren: new Set(), - previousEquals: itemEqualityComparator(actual), - }; + if (currentlySeen) { + onDidChangeTest.fire(wrapper); + return; + } - this.testItemToInternal.set(actual, internal); - this.testIdToInternal.set(internal.id, internal); - this.diff.push([TestDiffOpType.Add, { id: internal.id, parent, providerId, item: internal.item }]); - } else if (!internal.previousEquals(actual)) { - internal.item = TestItem.from(actual); - internal.previousEquals = itemEqualityComparator(actual); - this.diff.push([TestDiffOpType.Update, { id: internal.id, parent, providerId, item: internal.item }]); - } - - // If there are children, track which ones are deleted - // and recursively and/update them. - if (actual.children) { - const deletedChildren = internal.previousChildren; - const currentChildren = new Set(); - for (const child of actual.children) { - const c = this.addItem(child, providerId, internal.id); - deletedChildren.delete(c.id); - currentChildren.add(c.id); + // Fire the event to say that the current visible parent has changed. + onDidChangeTest.fire(wrapper.visibleParent); + return; } - for (const child of deletedChildren) { - this.removeItembyId(child); + const previousParent = wrapper.visibleParent; + wrapper.reset(); + const currentlySeen = wrapper.hasNodeMatchingFilter; + + // It wasn't previously seen and isn't currently seen so + // nothing has actually changed. + if (!currentlySeen) { + return; } - internal.previousChildren = currentChildren; - } + // The test is now visible so we need to refresh the cache + // of the previous visible parent and fire that it has changed. + previousParent.reset(); + onDidChangeTest.fire(previousParent); + }); + return { + root: TestItemFilteredWrapper.getWrapperForTestItem(workspaceHierarchy.root, document), + dispose: () => { + onDidChangeTest.dispose(); + TestItemFilteredWrapper.removeFilter(document); + }, + onDidChangeTest: onDidChangeTest.event + }; + } +} - return internal; +/* + * A class which wraps a vscode.TestItem that provides the ability to filter a TestItem's children + * to only the children that are located in a certain vscode.Uri. + */ +export class TestItemFilteredWrapper implements vscode.TestItem { + private static wrapperMap = new WeakMap>(); + public static removeFilter(document: vscode.TextDocument): void { + this.wrapperMap.delete(document); } - private removeItembyId(id: string) { - this.diff.push([TestDiffOpType.Remove, id]); - - const queue = [this.testIdToInternal.get(id)]; - while (queue.length) { - const item = queue.pop(); - if (!item) { - continue; - } - - this.testIdToInternal.delete(item.id); - this.testItemToInternal.delete(item.actual); - for (const child of item.previousChildren) { - queue.push(this.testIdToInternal.get(child)); - } + // Wraps the TestItem specified in a TestItemFilteredWrapper and pulls from a cache if it already exists. + public static getWrapperForTestItem(item: vscode.TestItem, filterDocument: vscode.TextDocument, parent?: TestItemFilteredWrapper): TestItemFilteredWrapper { + let innerMap = this.wrapperMap.get(filterDocument); + if (innerMap?.has(item)) { + return innerMap.get(item)!; } + + if (!innerMap) { + innerMap = new WeakMap(); + this.wrapperMap.set(filterDocument, innerMap); + + } + + const w = new TestItemFilteredWrapper(item, filterDocument, parent); + innerMap.set(item, w); + return w; } - @throttle(200) - protected throttleSendDiff() { - const diff = this.collectDiff(); - if (diff.length) { - this.publishDiff(diff); + public get label() { + return this.actual.label; + } + + public get debuggable() { + return this.actual.debuggable; + } + + public get description() { + return this.actual.description; + } + + public get location() { + return this.actual.location; + } + + public get runnable() { + return this.actual.runnable; + } + + public get state() { + return this.actual.state; + } + + public get children() { + // We only want children that match the filter. + return this.getWrappedChildren().filter(child => child.hasNodeMatchingFilter); + } + + public get visibleParent(): TestItemFilteredWrapper { + return this.hasNodeMatchingFilter ? this : this.parent!.visibleParent; + } + + private matchesFilter: boolean | undefined; + + // Determines if the TestItem matches the filter. This would be true if: + // 1. We don't have a parent (because the root is the workspace root node) + // 2. The URI of the current node matches the filter URI + // 3. Some child of the current node matches the filter URI + public get hasNodeMatchingFilter(): boolean { + if (this.matchesFilter === undefined) { + this.matchesFilter = !this.parent + || this.actual.location?.uri.toString() === this.filterDocument.uri.toString() + || this.getWrappedChildren().some(child => child.hasNodeMatchingFilter); } + + return this.matchesFilter; + } + + // Reset the cache of whether or not you can see a node from a particular node + // up to it's visible parent. + public reset(): void { + if (this !== this.visibleParent) { + this.parent?.reset(); + } + this.matchesFilter = undefined; + } + + + private constructor(public readonly actual: vscode.TestItem, private filterDocument: vscode.TextDocument, private readonly parent?: TestItemFilteredWrapper) { + this.getWrappedChildren(); + } + + private getWrappedChildren() { + return this.actual.children?.map(t => TestItemFilteredWrapper.getWrapperForTestItem(t, this.filterDocument, this)) || []; } } @@ -382,7 +415,7 @@ export class SingleUseTestCollection implements IDisposable { interface MirroredCollectionTestItem extends IncrementalTestCollectionItem { revived: vscode.TestItem; depth: number; - wrapped?: vscode.TestItem; + wrapped?: vscode.RequiredTestItem; } class MirroredChangeCollector extends IncrementalChangeCollector { @@ -411,7 +444,7 @@ class MirroredChangeCollector extends IncrementalChangeCollector): vscode.TestItem[] { - let output: vscode.TestItem[] = []; + public getAllAsTestItem(itemIds: Iterable): vscode.RequiredTestItem[] { + let output: vscode.RequiredTestItem[] = []; for (const itemId of itemIds) { const item = this.items.get(itemId); if (item) { @@ -577,7 +610,7 @@ export class MirroredTestCollection extends AbstractIncrementalTestCollection { const MirroredItemId = Symbol('MirroredItemId'); -class ExtHostTestItem implements vscode.TestItem, RequiredTestItem { +class TestItemFromMirror implements vscode.RequiredTestItem { readonly #internal: MirroredCollectionTestItem; readonly #collection: MirroredTestCollection; + public get id() { return this.#internal.revived.id!; } public get label() { return this.#internal.revived.label; } public get description() { return this.#internal.revived.description; } public get state() { return this.#internal.revived.state; } @@ -627,14 +661,18 @@ class ExtHostTestItem implements vscode.TestItem, RequiredTestItem { } public toJSON() { - const serialized: RequiredTestItem = { + const serialized: vscode.RequiredTestItem & TestIdWithProvider = { + id: this.id, label: this.label, description: this.description, state: this.state, location: this.location, runnable: this.runnable, debuggable: this.debuggable, - children: this.children.map(c => (c as ExtHostTestItem).toJSON()), + children: this.children.map(c => (c as TestItemFromMirror).toJSON()), + + providerId: this.#internal.providerId, + testId: this.#internal.id, }; return serialized; @@ -655,6 +693,7 @@ abstract class AbstractTestObserverFactory { const resourceKey = resourceUri.toString(); const resource = this.resources.get(resourceKey) ?? this.createObserverData(resourceUri); + resource.pendingDeletion?.dispose(); resource.observers++; return { @@ -778,16 +817,9 @@ class TextDocumentTestObserverFactory extends AbstractTestObserverFactory { const uriString = resourceUri.toString(); this.diffListeners.set(uriString, onDiff); - const disposeListener = this.documents.onDidRemoveDocuments(evt => { - if (evt.some(delta => delta.document.uri.toString() === uriString)) { - this.unlisten(resourceUri); - } - }); - this.proxy.$subscribeToDiffs(ExtHostTestingResource.TextDocument, resourceUri); return new Disposable(() => { this.proxy.$unsubscribeFromDiffs(ExtHostTestingResource.TextDocument, resourceUri); - disposeListener.dispose(); this.diffListeners.delete(uriString); }); } diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index 63d30701..ca1914d2 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -21,6 +21,7 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions' import { MarkdownString } from 'vs/workbench/api/common/extHostTypeConverters'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Command } from 'vs/editor/common/modes'; type TreeItemHandle = string; @@ -132,12 +133,12 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { return treeView.hasResolve; } - $resolve(treeViewId: string, treeItemHandle: string): Promise { + $resolve(treeViewId: string, treeItemHandle: string, token: vscode.CancellationToken): Promise { const treeView = this.treeViews.get(treeViewId); if (!treeView) { throw new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', treeViewId)); } - return treeView.resolveTreeItem(treeItemHandle); + return treeView.resolveTreeItem(treeItemHandle, token); } $setExpanded(treeViewId: string, treeItemHandle: string, expanded: boolean): void { @@ -184,6 +185,7 @@ interface TreeNode extends IDisposable { extensionItem: vscode.TreeItem; parent: TreeNode | Root; children?: TreeNode[]; + disposableStore: DisposableStore; } class ExtHostTreeView extends Disposable { @@ -370,7 +372,7 @@ class ExtHostTreeView extends Disposable { return !!this.dataProvider.resolveTreeItem; } - async resolveTreeItem(treeItemHandle: string): Promise { + async resolveTreeItem(treeItemHandle: string, token: vscode.CancellationToken): Promise { if (!this.dataProvider.resolveTreeItem) { return; } @@ -378,9 +380,10 @@ class ExtHostTreeView extends Disposable { if (element) { const node = this.nodes.get(element); if (node) { - const resolve = await this.dataProvider.resolveTreeItem(node.extensionItem, element) ?? node.extensionItem; - // Resolvable elements. Currently only tooltip. + const resolve = await this.dataProvider.resolveTreeItem(node.extensionItem, element, token) ?? node.extensionItem; + // Resolvable elements. Currently only tooltip and command. node.item.tooltip = this.getTooltip(resolve.tooltip); + node.item.command = this.getCommand(node.disposableStore, resolve.command); return node.item; } } @@ -573,8 +576,12 @@ class ExtHostTreeView extends Disposable { return tooltip; } + private getCommand(disposable: DisposableStore, command?: vscode.Command): Command | undefined { + return command ? this.commands.toInternal(command, disposable) : undefined; + } + private createTreeNode(element: T, extensionTreeItem: vscode.TreeItem, parent: TreeNode | Root): TreeNode { - const disposable = new DisposableStore(); + const disposableStore = new DisposableStore(); const handle = this.createHandle(element, extensionTreeItem, parent); const icon = this.getLightIconPath(extensionTreeItem); const item: ITreeItem = { @@ -584,7 +591,7 @@ class ExtHostTreeView extends Disposable { description: extensionTreeItem.description, resourceUri: extensionTreeItem.resourceUri, tooltip: this.getTooltip(extensionTreeItem.tooltip), - command: extensionTreeItem.command ? this.commands.toInternal(extensionTreeItem.command, disposable) : undefined, + command: this.getCommand(disposableStore, extensionTreeItem.command), contextValue: extensionTreeItem.contextValue, icon, iconDark: this.getDarkIconPath(extensionTreeItem) || icon, @@ -598,7 +605,8 @@ class ExtHostTreeView extends Disposable { extensionItem: extensionTreeItem, parent, children: undefined, - dispose(): void { disposable.dispose(); } + disposableStore, + dispose(): void { disposableStore.dispose(); } }; } diff --git a/src/vs/workbench/api/common/extHostTunnelService.ts b/src/vs/workbench/api/common/extHostTunnelService.ts index 29a50ae2..721cbaba 100644 --- a/src/vs/workbench/api/common/extHostTunnelService.ts +++ b/src/vs/workbench/api/common/extHostTunnelService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtHostTunnelServiceShape, MainContext, MainThreadTunnelServiceShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostTunnelServiceShape } from 'vs/workbench/api/common/extHost.protocol'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import * as vscode from 'vscode'; import { RemoteTunnel, TunnelCreationOptions, TunnelOptions } from 'vs/platform/remote/common/tunnel'; @@ -11,18 +11,27 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; export interface TunnelDto { remoteAddress: { port: number, host: string }; localAddress: { port: number, host: string } | string; + public: boolean; } export namespace TunnelDto { export function fromApiTunnel(tunnel: vscode.Tunnel): TunnelDto { - return { remoteAddress: tunnel.remoteAddress, localAddress: tunnel.localAddress }; + return { remoteAddress: tunnel.remoteAddress, localAddress: tunnel.localAddress, public: !!tunnel.public }; } export function fromServiceTunnel(tunnel: RemoteTunnel): TunnelDto { - return { remoteAddress: { host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }, localAddress: tunnel.localAddress }; + return { + remoteAddress: { + host: tunnel.tunnelRemoteHost, + port: tunnel.tunnelRemotePort + }, + localAddress: tunnel.localAddress, + public: tunnel.public + }; } } @@ -44,12 +53,13 @@ export const IExtHostTunnelService = createDecorator('IEx export class ExtHostTunnelService implements IExtHostTunnelService { declare readonly _serviceBrand: undefined; onDidChangeTunnels: vscode.Event = (new Emitter()).event; - private readonly _proxy: MainThreadTunnelServiceShape; constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, ) { - this._proxy = extHostRpc.getProxy(MainContext.MainThreadTunnelService); + } + async $applyCandidateFilter(candidates: CandidatePort[]): Promise { + return candidates; } async openTunnel(extension: IExtensionDescription, forward: TunnelOptions): Promise { @@ -59,10 +69,10 @@ export class ExtHostTunnelService implements IExtHostTunnelService { return []; } async setTunnelExtensionFunctions(provider: vscode.RemoteAuthorityResolver | undefined): Promise { - await this._proxy.$tunnelServiceReady(); return { dispose: () => { } }; } - $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise | undefined { return undefined; } + async $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise { return undefined; } async $closeTunnel(remote: { host: string, port: number }): Promise { } async $onDidTunnelsChange(): Promise { } + async $registerCandidateFinder(): Promise { } } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index d80dfae4..d1512e59 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -1015,6 +1015,29 @@ export namespace SignatureHelp { } } +export namespace InlineHint { + + export function from(hint: vscode.InlineHint): modes.InlineHint { + return { + text: hint.text, + range: Range.from(hint.range), + description: hint.description && MarkdownString.fromStrict(hint.description), + whitespaceBefore: hint.whitespaceBefore, + whitespaceAfter: hint.whitespaceAfter + }; + } + + export function to(hint: modes.InlineHint): vscode.InlineHint { + return new types.InlineHint( + hint.text, + Range.to(hint.range), + htmlContent.isMarkdownString(hint.description) ? MarkdownString.to(hint.description) : hint.description, + hint.whitespaceBefore, + hint.whitespaceAfter + ); + } +} + export namespace DocumentLink { export function from(link: vscode.DocumentLink): modes.ILink { @@ -1389,19 +1412,21 @@ export namespace TestState { export namespace TestItem { - export function from(item: vscode.TestItem): ITestItem { + export function from(item: vscode.TestItem, parentExtId?: string): ITestItem { return { + extId: item.id ?? (parentExtId ? `${parentExtId}\0${item.label}` : item.label), label: item.label, location: item.location ? location.from(item.location) : undefined, - debuggable: item.debuggable, + debuggable: item.debuggable ?? false, description: item.description, - runnable: item.runnable, + runnable: item.runnable ?? true, state: TestState.from(item.state), }; } - export function to(item: ITestItem): vscode.TestItem { + export function toShallow(item: ITestItem): Omit { return { + id: item.extId, label: item.label, location: item.location && location.to({ range: item.location.range, diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 069c5658..5d1bb34c 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -13,7 +13,7 @@ import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from 'vs/platform/files/common/files'; import { RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { addIdToOutput, CellEditType, ICellEditOperation, IDisplayOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { addIdToOutput, CellEditType, ICellEditOperation, IDisplayOutput, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import type * as vscode from 'vscode'; function es5ClassCompat(target: Function): any { @@ -638,7 +638,7 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { // --- notebook replaceNotebookMetadata(uri: URI, value: vscode.NotebookDocumentMetadata, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: FileEditType.Cell, metadata, uri, notebookMetadata: value }); + this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.DocumentMetadata, metadata: { ...notebookDocumentMetadataDefaults, ...value } }, notebookMetadata: value }); } replaceNotebookCells(uri: URI, start: number, end: number, cells: vscode.NotebookCellData[], metadata?: vscode.WorkspaceEditEntryMetadata): void { @@ -1373,6 +1373,23 @@ export enum SignatureHelpTriggerKind { ContentChange = 3, } +@es5ClassCompat +export class InlineHint { + text: string; + range: Range; + description?: string | vscode.MarkdownString; + whitespaceBefore?: boolean; + whitespaceAfter?: boolean; + + constructor(text: string, range: Range, description?: string | vscode.MarkdownString, whitespaceBefore?: boolean, whitespaceAfter?: boolean) { + this.text = text; + this.range = range; + this.description = description; + this.whitespaceBefore = whitespaceBefore; + this.whitespaceAfter = whitespaceAfter; + } +} + export enum CompletionTriggerKind { Invoke = 0, TriggerCharacter = 1, @@ -2298,9 +2315,6 @@ export class FunctionBreakpoint extends Breakpoint { constructor(functionName: string, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string) { super(enabled, condition, hitCondition, logMessage); - if (!functionName) { - throw illegalArgument('functionName'); - } this.functionName = functionName; } } @@ -2858,7 +2872,8 @@ export enum NotebookCellStatusBarAlignment { export enum NotebookEditorRevealType { Default = 0, InCenter = 1, - InCenterIfOutsideViewport = 2 + InCenterIfOutsideViewport = 2, + AtTop = 3 } @@ -2924,11 +2939,12 @@ export class LinkedEditingRanges { //#region Testing export enum TestRunState { Unset = 0, - Running = 1, - Passed = 2, - Failed = 3, - Skipped = 4, - Errored = 5 + Queued = 1, + Running = 2, + Passed = 3, + Failed = 4, + Skipped = 5, + Errored = 6 } export enum TestMessageSeverity { @@ -2963,15 +2979,15 @@ export class TestState { } } -type AllowedUndefined = 'description' | 'location'; - -/** - * Test item without any optional properties. Only some properties are - * permitted to be undefined, but they must still exist. - */ -export type RequiredTestItem = { - [K in keyof Required]: K extends AllowedUndefined ? vscode.TestItem[K] : Required[K] -}; +export type RequiredTestItem = vscode.RequiredTestItem; +export type TestItem = vscode.TestItem; //#endregion + +export enum ExternalUriOpenerPriority { + None = 0, + Option = 1, + Default = 2, + Preferred = 3, +} diff --git a/src/vs/workbench/api/common/extHostUriOpener.ts b/src/vs/workbench/api/common/extHostUriOpener.ts new file mode 100644 index 00000000..81bbc6f4 --- /dev/null +++ b/src/vs/workbench/api/common/extHostUriOpener.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { toDisposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import * as modes from 'vs/editor/common/modes'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import type * as vscode from 'vscode'; +import { ExtHostUriOpenersShape, IMainContext, MainContext, MainThreadUriOpenersShape } from './extHost.protocol'; + + +export class ExtHostUriOpeners implements ExtHostUriOpenersShape { + + private static readonly supportedSchemes = new Set([Schemas.http, Schemas.https]); + + private readonly _proxy: MainThreadUriOpenersShape; + + private readonly _openers = new Map(); + + constructor( + mainContext: IMainContext, + ) { + this._proxy = mainContext.getProxy(MainContext.MainThreadUriOpeners); + } + + registerExternalUriOpener( + extensionId: ExtensionIdentifier, + id: string, + opener: vscode.ExternalUriOpener, + metadata: vscode.ExternalUriOpenerMetadata, + ): vscode.Disposable { + if (this._openers.has(id)) { + throw new Error(`Opener with id '${id}' already registered`); + } + + const invalidScheme = metadata.schemes.find(scheme => !ExtHostUriOpeners.supportedSchemes.has(scheme)); + if (invalidScheme) { + throw new Error(`Scheme '${invalidScheme}' is not supported. Only http and https are currently supported.`); + } + + this._openers.set(id, opener); + this._proxy.$registerUriOpener(id, metadata.schemes, extensionId, metadata.label); + + return toDisposable(() => { + this._openers.delete(id); + this._proxy.$unregisterUriOpener(id); + }); + } + + async $canOpenUri(id: string, uriComponents: UriComponents, token: CancellationToken): Promise { + const opener = this._openers.get(id); + if (!opener) { + throw new Error(`Unknown opener with id: ${id}`); + } + + const uri = URI.revive(uriComponents); + return opener.canOpenExternalUri(uri, token); + } + + async $openUri(id: string, context: { resolvedUri: UriComponents, sourceUri: UriComponents }, token: CancellationToken): Promise { + const opener = this._openers.get(id); + if (!opener) { + throw new Error(`Unknown opener id: '${id}'`); + } + + return opener.openExternalUri(URI.revive(context.resolvedUri), { + sourceUri: URI.revive(context.sourceUri) + }, token); + } +} diff --git a/src/vs/workbench/api/common/extHostWebviewPanels.ts b/src/vs/workbench/api/common/extHostWebviewPanels.ts index 797702b8..dfdd948b 100644 --- a/src/vs/workbench/api/common/extHostWebviewPanels.ts +++ b/src/vs/workbench/api/common/extHostWebviewPanels.ts @@ -287,8 +287,8 @@ export class ExtHostWebviewPanels implements extHostProtocol.ExtHostWebviewPanel await serializer.deserializeWebviewPanel(revivedPanel, state); } - public createNewWebviewPanel(webviewHandle: string, viewType: string, title: string, position: number, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, webview: ExtHostWebview) { - const panel = new ExtHostWebviewPanel(webviewHandle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview); + public createNewWebviewPanel(webviewHandle: string, viewType: string, title: string, position: vscode.ViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, webview: ExtHostWebview) { + const panel = new ExtHostWebviewPanel(webviewHandle, this._proxy, viewType, title, position, options, webview); this._webviewPanels.set(webviewHandle, panel); return panel; } diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index 3e0de07e..c03c6be5 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -10,7 +10,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { TernarySearchTree } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; import { Counter } from 'vs/base/common/numbers'; -import { basename, basenameOrAuthority, dirname, isEqual, relativePath } from 'vs/base/common/resources'; +import { basename, basenameOrAuthority, dirname, ExtUri, relativePath } from 'vs/base/common/resources'; import { compare } from 'vs/base/common/strings'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; @@ -37,27 +37,27 @@ export interface IExtHostWorkspaceProvider { resolveProxy(url: string): Promise; } -function isFolderEqual(folderA: URI, folderB: URI): boolean { - return isEqual(folderA, folderB); +function isFolderEqual(folderA: URI, folderB: URI, extHostFileSystemInfo: IExtHostFileSystemInfo): boolean { + return new ExtUri(uri => ignorePathCasing(uri, extHostFileSystemInfo)).isEqual(folderA, folderB); } -function compareWorkspaceFolderByUri(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder): number { - return isFolderEqual(a.uri, b.uri) ? 0 : compare(a.uri.toString(), b.uri.toString()); +function compareWorkspaceFolderByUri(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder, extHostFileSystemInfo: IExtHostFileSystemInfo): number { + return isFolderEqual(a.uri, b.uri, extHostFileSystemInfo) ? 0 : compare(a.uri.toString(), b.uri.toString()); } -function compareWorkspaceFolderByUriAndNameAndIndex(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder): number { +function compareWorkspaceFolderByUriAndNameAndIndex(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder, extHostFileSystemInfo: IExtHostFileSystemInfo): number { if (a.index !== b.index) { return a.index < b.index ? -1 : 1; } - return isFolderEqual(a.uri, b.uri) ? compare(a.name, b.name) : compare(a.uri.toString(), b.uri.toString()); + return isFolderEqual(a.uri, b.uri, extHostFileSystemInfo) ? compare(a.name, b.name) : compare(a.uri.toString(), b.uri.toString()); } -function delta(oldFolders: vscode.WorkspaceFolder[], newFolders: vscode.WorkspaceFolder[], compare: (a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder) => number): { removed: vscode.WorkspaceFolder[], added: vscode.WorkspaceFolder[] } { - const oldSortedFolders = oldFolders.slice(0).sort(compare); - const newSortedFolders = newFolders.slice(0).sort(compare); +function delta(oldFolders: vscode.WorkspaceFolder[], newFolders: vscode.WorkspaceFolder[], compare: (a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder, extHostFileSystemInfo: IExtHostFileSystemInfo) => number, extHostFileSystemInfo: IExtHostFileSystemInfo): { removed: vscode.WorkspaceFolder[], added: vscode.WorkspaceFolder[] } { + const oldSortedFolders = oldFolders.slice(0).sort((a, b) => compare(a, b, extHostFileSystemInfo)); + const newSortedFolders = newFolders.slice(0).sort((a, b) => compare(a, b, extHostFileSystemInfo)); - return arrayDelta(oldSortedFolders, newSortedFolders, compare); + return arrayDelta(oldSortedFolders, newSortedFolders, (a, b) => compare(a, b, extHostFileSystemInfo)); } function ignorePathCasing(uri: URI, extHostFileSystemInfo: IExtHostFileSystemInfo): boolean { @@ -87,7 +87,7 @@ class ExtHostWorkspaceImpl extends Workspace { if (previousConfirmedWorkspace) { folders.forEach((folderData, index) => { const folderUri = URI.revive(folderData.uri); - const existingFolder = ExtHostWorkspaceImpl._findFolder(previousUnconfirmedWorkspace || previousConfirmedWorkspace, folderUri); + const existingFolder = ExtHostWorkspaceImpl._findFolder(previousUnconfirmedWorkspace || previousConfirmedWorkspace, folderUri, extHostFileSystemInfo); if (existingFolder) { existingFolder.name = folderData.name; @@ -106,15 +106,15 @@ class ExtHostWorkspaceImpl extends Workspace { newWorkspaceFolders.sort((f1, f2) => f1.index < f2.index ? -1 : 1); const workspace = new ExtHostWorkspaceImpl(id, name, newWorkspaceFolders, configuration ? URI.revive(configuration) : null, !!isUntitled, uri => ignorePathCasing(uri, extHostFileSystemInfo)); - const { added, removed } = delta(oldWorkspace ? oldWorkspace.workspaceFolders : [], workspace.workspaceFolders, compareWorkspaceFolderByUri); + const { added, removed } = delta(oldWorkspace ? oldWorkspace.workspaceFolders : [], workspace.workspaceFolders, compareWorkspaceFolderByUri, extHostFileSystemInfo); return { workspace, added, removed }; } - private static _findFolder(workspace: ExtHostWorkspaceImpl, folderUriToFind: URI): MutableWorkspaceFolder | undefined { + private static _findFolder(workspace: ExtHostWorkspaceImpl, folderUriToFind: URI, extHostFileSystemInfo: IExtHostFileSystemInfo): MutableWorkspaceFolder | undefined { for (let i = 0; i < workspace.folders.length; i++) { const folder = workspace.workspaceFolders[i]; - if (isFolderEqual(folder.uri, folderUriToFind)) { + if (isFolderEqual(folder.uri, folderUriToFind, extHostFileSystemInfo)) { return folder; } } @@ -254,7 +254,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac const validatedDistinctWorkspaceFoldersToAdd: { uri: vscode.Uri, name?: string }[] = []; if (Array.isArray(workspaceFoldersToAdd)) { workspaceFoldersToAdd.forEach(folderToAdd => { - if (URI.isUri(folderToAdd.uri) && !validatedDistinctWorkspaceFoldersToAdd.some(f => isFolderEqual(f.uri, folderToAdd.uri))) { + if (URI.isUri(folderToAdd.uri) && !validatedDistinctWorkspaceFoldersToAdd.some(f => isFolderEqual(f.uri, folderToAdd.uri, this._extHostFileSystemInfo))) { validatedDistinctWorkspaceFoldersToAdd.push({ uri: folderToAdd.uri, name: folderToAdd.name || basenameOrAuthority(folderToAdd.uri) }); } }); @@ -283,13 +283,13 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac for (let i = 0; i < newWorkspaceFolders.length; i++) { const folder = newWorkspaceFolders[i]; - if (newWorkspaceFolders.some((otherFolder, index) => index !== i && isFolderEqual(folder.uri, otherFolder.uri))) { + if (newWorkspaceFolders.some((otherFolder, index) => index !== i && isFolderEqual(folder.uri, otherFolder.uri, this._extHostFileSystemInfo))) { return false; // cannot add the same folder multiple times } } newWorkspaceFolders.forEach((f, index) => f.index = index); // fix index - const { added, removed } = delta(currentWorkspaceFolders, newWorkspaceFolders, compareWorkspaceFolderByUriAndNameAndIndex); + const { added, removed } = delta(currentWorkspaceFolders, newWorkspaceFolders, compareWorkspaceFolderByUriAndNameAndIndex, this._extHostFileSystemInfo); if (added.length === 0 && removed.length === 0) { return false; // nothing actually changed } diff --git a/src/vs/workbench/api/common/shared/workspaceContains.ts b/src/vs/workbench/api/common/shared/workspaceContains.ts index 629c9994..74a283dc 100644 --- a/src/vs/workbench/api/common/shared/workspaceContains.ts +++ b/src/vs/workbench/api/common/shared/workspaceContains.ts @@ -119,8 +119,7 @@ export function checkGlobFileExists( const queryBuilder = instantiationService.createInstance(QueryBuilder); const query = queryBuilder.file(folders.map(folder => toWorkspaceFolder(URI.revive(folder))), { _reason: 'checkExists', - includePattern: includes.join(', '), - expandPatterns: true, + includePattern: includes, exists: true }); diff --git a/src/vs/workbench/api/node/extHostCLIServer.ts b/src/vs/workbench/api/node/extHostCLIServer.ts index bb5ec1cd..b3424a77 100644 --- a/src/vs/workbench/api/node/extHostCLIServer.ts +++ b/src/vs/workbench/api/node/extHostCLIServer.ts @@ -39,7 +39,15 @@ export interface RunCommandPipeArgs { args: any[]; } -export type PipeCommand = OpenCommandPipeArgs | StatusPipeArgs | RunCommandPipeArgs | OpenExternalCommandPipeArgs; +export interface ExtensionManagementPipeArgs { + type: 'extensionManagement'; + list?: { showVersions?: boolean, category?: string; }; + install?: string[]; + uninstall?: string[]; + force?: boolean; +} + +export type PipeCommand = OpenCommandPipeArgs | StatusPipeArgs | RunCommandPipeArgs | OpenExternalCommandPipeArgs | ExtensionManagementPipeArgs; export interface ICommandsExecuter { executeCommand(id: string, ...args: any[]): Promise; @@ -95,6 +103,10 @@ export class CLIServerBase { this.runCommand(data, res) .catch(this.logService.error); break; + case 'extensionManagement': + this.manageExtensions(data, res) + .catch(this.logService.error); + break; default: res.writeHead(404); res.write(`Unknown message type: ${data.type}`, err => { @@ -143,14 +155,34 @@ export class CLIServerBase { res.end(); } - private openExternal(data: OpenExternalCommandPipeArgs, res: http.ServerResponse) { + private async openExternal(data: OpenExternalCommandPipeArgs, res: http.ServerResponse) { for (const uri of data.uris) { - this._commands.executeCommand('_workbench.openExternal', URI.parse(uri), { allowTunneling: true }); + await this._commands.executeCommand('_remoteCLI.openExternal', URI.parse(uri), { allowTunneling: true }); } res.writeHead(200); res.end(); } + private async manageExtensions(data: ExtensionManagementPipeArgs, res: http.ServerResponse) { + console.log('server: manageExtensions'); + try { + const toExtOrVSIX = (inputs: string[] | undefined) => inputs?.map(input => /\.vsix$/i.test(input) ? URI.parse(input) : input); + const commandArgs = { + list: data.list, + install: toExtOrVSIX(data.install), + uninstall: toExtOrVSIX(data.uninstall), + force: data.force + }; + const output = await this._commands.executeCommand('_remoteCLI.manageExtensions', commandArgs, { allowTunneling: true }); + res.writeHead(200); + res.write(output); + } catch (e) { + res.writeHead(500); + res.write(String(e)); + } + res.end(); + } + private async getStatus(data: StatusPipeArgs, res: http.ServerResponse) { try { const status = await this._commands.executeCommand('_issues.getSystemStatus'); diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index ff76f109..992c6377 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -104,7 +104,7 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { cwdForPrepareCommand = args.cwd; } - terminal.show(); + terminal.show(true); const shellProcessId = await terminal.processId; diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 3d92a699..d9377b68 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as performance from 'vs/base/common/performance'; import { createApiFactoryAndRegisterActors } from 'vs/workbench/api/common/extHost.api.impl'; import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor'; import { MainContext } from 'vs/workbench/api/common/extHost.protocol'; @@ -13,7 +14,7 @@ import { ExtHostDownloadService } from 'vs/workbench/api/node/extHostDownloadSer import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes'; class NodeModuleRequireInterceptor extends RequireInterceptor { @@ -62,10 +63,12 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { // Module loading tricks const interceptor = this._instaService.createInstance(NodeModuleRequireInterceptor, extensionApiFactory, this._registry); await interceptor.install(); + performance.mark('code/extHost/didInitAPI'); // Do this when extension service exists, but extensions are not being activated yet. const configProvider = await this._extHostConfiguration.getConfigProvider(); await connectProxyResolver(this._extHostWorkspace, configProvider, this, this._logService, this._mainThreadTelemetryProxy, this._initData); + performance.mark('code/extHost/didInitProxyResolver'); // Use IPC messages to forward console-calls, note that the console is // already patched to use`process.send()` @@ -84,7 +87,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { return extensionDescription.main; } - protected _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { + protected _loadCommonJSModule(extensionId: ExtensionIdentifier | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { if (module.scheme !== Schemas.file) { throw new Error(`Cannot load URI: '${module}', must be of file-scheme`); } @@ -93,10 +96,16 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { this._logService.info(`ExtensionService#loadCommonJSModule ${module.toString(true)}`); this._logService.flush(); try { + if (extensionId) { + performance.mark(`code/extHost/willLoadExtensionCode/${extensionId.value}`); + } r = require.__$__nodeRequire(module.fsPath); } catch (e) { return Promise.reject(e); } finally { + if (extensionId) { + performance.mark(`code/extHost/didLoadExtensionCode/${extensionId.value}`); + } activationTimesBuilder.codeLoadingStop(); } return Promise.resolve(r); diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index 9d5951b8..260ace16 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -50,11 +50,12 @@ export class ExtHostTask extends ExtHostTaskBase { } public async executeTask(extension: IExtensionDescription, task: vscode.Task): Promise { - if (!task.execution) { + const tTask = (task as types.Task); + + if (!task.execution && (tTask._id === undefined)) { throw new Error('Tasks to execute must include an execution'); } - const tTask = (task as types.Task); // We have a preserved ID. So the task didn't change. if (tTask._id !== undefined) { // Always get the task execution first to prevent timing issues when retrieving it later @@ -121,7 +122,7 @@ export class ExtHostTask extends ExtHostTaskBase { private async getVariableResolver(workspaceFolders: vscode.WorkspaceFolder[]): Promise { if (this._variableResolver === undefined) { const configProvider = await this._configurationService.getConfigProvider(); - this._variableResolver = new ExtHostVariableResolverService(workspaceFolders, this._editorService, configProvider, process.env as IProcessEnvironment); + this._variableResolver = new ExtHostVariableResolverService(workspaceFolders, this._editorService, configProvider, process.env as IProcessEnvironment, this.workspaceService); } return this._variableResolver; } diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 01a34ff3..adad271b 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -17,13 +17,15 @@ import { ExtHostWorkspace, IExtHostWorkspace } from 'vs/workbench/api/common/ext import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostDebugService'; import { ExtHostDocumentsAndEditors, IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { getSystemShell, detectAvailableShells } from 'vs/workbench/contrib/terminal/node/terminal'; +import { detectAvailableShells } from 'vs/workbench/contrib/terminal/node/terminal'; import { getMainProcessParentEnv } from 'vs/workbench/contrib/terminal/node/terminalEnvironment'; import { BaseExtHostTerminalService, ExtHostTerminal } from 'vs/workbench/api/common/extHostTerminalService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { getSystemShell, getSystemShellSync } from 'vs/base/node/shell'; +import { generateUuid } from 'vs/base/common/uuid'; export class ExtHostTerminalService extends BaseExtHostTerminalService { @@ -32,6 +34,7 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { // TODO: Pull this from main side private _isWorkspaceShellAllowed: boolean = false; + private _defaultShell: string | undefined; constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, @@ -42,20 +45,26 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { @IExtHostInitDataService private _extHostInitDataService: IExtHostInitDataService ) { super(true, extHostRpc); + + // Getting the SystemShell is an async operation, however, the ExtHost terminal service is mostly synchronous + // and the API `vscode.env.shell` is also synchronous. The default shell _should_ be set when extensions are + // starting up but if not, we run getSystemShellSync below which gets a sane default. + getSystemShell(platform.platform).then(s => this._defaultShell = s); + this._updateLastActiveWorkspace(); this._updateVariableResolver(); this._registerListeners(); } public createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal { - const terminal = new ExtHostTerminal(this._proxy, { name, shellPath, shellArgs }, name); + const terminal = new ExtHostTerminal(this._proxy, generateUuid(), { name, shellPath, shellArgs }, name); this._terminals.push(terminal); terminal.create(shellPath, shellArgs); return terminal; } public createTerminalFromOptions(options: vscode.TerminalOptions, isFeatureTerminal?: boolean): vscode.Terminal { - const terminal = new ExtHostTerminal(this._proxy, options, options.name); + const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name); this._terminals.push(terminal); terminal.create( withNullAsUndefined(options.shellPath), @@ -76,10 +85,11 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { .inspect(key.substr(key.lastIndexOf('.') + 1)); return this._apiInspectConfigToPlain(setting); }; + return terminalEnvironment.getDefaultShell( fetchSetting, this._isWorkspaceShellAllowed, - getSystemShell(platform.platform), + this._defaultShell ?? getSystemShellSync(platform.platform), process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'), process.env.windir, terminalEnvironment.createVariableResolver(this._lastActiveWorkspace, this._variableResolver), @@ -139,7 +149,8 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { executable: shellLaunchConfigDto.executable, args: shellLaunchConfigDto.args, cwd: typeof shellLaunchConfigDto.cwd === 'string' ? shellLaunchConfigDto.cwd : URI.revive(shellLaunchConfigDto.cwd), - env: shellLaunchConfigDto.env + env: shellLaunchConfigDto.env, + flowControl: shellLaunchConfigDto.flowControl }; // Merge in shell and args from settings diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index 922451d1..b7a05876 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -12,12 +12,16 @@ import { URI } from 'vs/base/common/uri'; import { exec } from 'child_process'; import * as resources from 'vs/base/common/resources'; import * as fs from 'fs'; +import * as pfs from 'vs/base/node/pfs'; import { isLinux } from 'vs/base/common/platform'; import { IExtHostTunnelService, TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; -import { asPromise } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; import { TunnelOptions, TunnelCreationOptions } from 'vs/platform/remote/common/tunnel'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { promisify } from 'util'; +import { MovingAverage } from 'vs/base/common/numbers'; +import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { ILogService } from 'vs/platform/log/common/log'; class ExtensionTunnel implements vscode.Tunnel { private _onDispose: Emitter = new Emitter(); @@ -26,14 +30,106 @@ class ExtensionTunnel implements vscode.Tunnel { constructor( public readonly remoteAddress: { port: number, host: string }, public readonly localAddress: { port: number, host: string } | string, - private readonly _dispose: () => void) { } + private readonly _dispose: () => Promise) { } - dispose(): void { + dispose(): Promise { this._onDispose.fire(); - this._dispose(); + return this._dispose(); } } +export function getSockets(stdout: string): { pid: number, socket: number }[] { + const lines = stdout.trim().split('\n'); + const mapped: { pid: number, socket: number }[] = []; + lines.forEach(line => { + const match = /\/proc\/(\d+)\/fd\/\d+ -> socket:\[(\d+)\]/.exec(line)!; + if (match && match.length >= 3) { + mapped.push({ + pid: parseInt(match[1], 10), + socket: parseInt(match[2], 10) + }); + } + }); + return mapped; +} + +export function loadListeningPorts(...stdouts: string[]): { socket: number, ip: string, port: number }[] { + const table = ([] as Record[]).concat(...stdouts.map(loadConnectionTable)); + return [ + ...new Map( + table.filter(row => row.st === '0A') + .map(row => { + const address = row.local_address.split(':'); + return { + socket: parseInt(row.inode, 10), + ip: parseIpAddress(address[0]), + port: parseInt(address[1], 16) + }; + }).map(port => [port.ip + ':' + port.port, port]) + ).values() + ]; +} + +export function parseIpAddress(hex: string): string { + let 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; +} + +export function loadConnectionTable(stdout: string): Record[] { + const lines = stdout.trim().split('\n'); + const names = lines.shift()!.trim().split(/\s+/) + .filter(name => name !== 'rx_queue' && name !== 'tm->when'); + const table = lines.map(line => line.trim().split(/\s+/).reduce((obj, value, i) => { + obj[names[i] || i] = value; + return obj; + }, {} as Record)); + return table; +} + +function knownExcludeCmdline(command: string): boolean { + return !!command.match(/.*\.vscode-server-[a-zA-Z]+\/bin.*/) + || (command.indexOf('out/vs/server/main.js') !== -1) + || (command.indexOf('_productName=VSCode') !== -1); +} + +export async function findPorts(tcp: string, tcp6: string, procSockets: string, processes: { pid: number, cwd: string, cmd: string }[]): Promise { + const connections: { socket: number, ip: string, port: number }[] = loadListeningPorts(tcp, tcp6); + const sockets = getSockets(procSockets); + + const socketMap = sockets.reduce((m, socket) => { + m[socket.socket] = socket; + return m; + }, {} as Record); + const processMap = processes.reduce((m, process) => { + m[process.pid] = process; + return m; + }, {} as Record); + + const ports: CandidatePort[] = []; + connections.filter((connection => socketMap[connection.socket])).forEach(({ socket, ip, port }) => { + const command = processMap[socketMap[socket].pid].cmd; + if (!knownExcludeCmdline(command)) { + ports.push({ host: ip, port, detail: processMap[socketMap[socket].pid].cmd, pid: socketMap[socket].pid }); + } + }); + return ports; +} + export class ExtHostTunnelService extends Disposable implements IExtHostTunnelService { readonly _serviceBrand: undefined; private readonly _proxy: MainThreadTunnelServiceShape; @@ -42,15 +138,17 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe private _extensionTunnels: Map> = new Map(); private _onDidChangeTunnels: Emitter = new Emitter(); onDidChangeTunnels: vscode.Event = this._onDidChangeTunnels.event; + private _candidateFindingEnabled: boolean = false; constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, - @IExtHostInitDataService initData: IExtHostInitDataService + @IExtHostInitDataService initData: IExtHostInitDataService, + @ILogService private readonly logService: ILogService ) { super(); this._proxy = extHostRpc.getProxy(MainContext.MainThreadTunnelService); - if (initData.remote.isRemote && initData.remote.authority) { - this.registerCandidateFinder(); + if (isLinux && initData.remote.isRemote && initData.remote.authority) { + this._proxy.$setCandidateFinder(); } } @@ -70,18 +168,30 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe return this._proxy.$getTunnels(); } - registerCandidateFinder(): void { - // Every two seconds, scan to see if the candidate ports have changed; - if (isLinux) { - let oldPorts: { host: string, port: number, detail: string }[] | undefined = undefined; - setInterval(async () => { - const newPorts = await this.findCandidatePorts(); - if (!oldPorts || (JSON.stringify(oldPorts) !== JSON.stringify(newPorts))) { - oldPorts = newPorts; - this._proxy.$onFoundNewCandidates(oldPorts.filter(async (candidate) => await this._showCandidatePort(candidate.host, candidate.port, candidate.detail))); - return; - } - }, 2000); + private calculateDelay(movingAverage: number) { + // Some local testing indicated that the moving average might be between 50-100 ms. + return Math.max(movingAverage * 20, 2000); + } + + async $registerCandidateFinder(enable: boolean): Promise { + if (enable && this._candidateFindingEnabled) { + // already enabled + return; + } + this._candidateFindingEnabled = enable; + // Regularly scan to see if the candidate ports have changed. + let movingAverage = new MovingAverage(); + let oldPorts: { host: string, port: number, detail: string }[] | undefined = undefined; + while (this._candidateFindingEnabled) { + const startTime = new Date().getTime(); + const newPorts = await this.findCandidatePorts(); + const timeTaken = new Date().getTime() - startTime; + movingAverage.update(timeTaken); + if (!oldPorts || (JSON.stringify(oldPorts) !== JSON.stringify(newPorts))) { + oldPorts = newPorts; + await this._proxy.$onFoundNewCandidates(oldPorts); + } + await (new Promise(resolve => setTimeout(() => resolve(), this.calculateDelay(movingAverage.value)))); } } @@ -89,15 +199,18 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe if (provider) { if (provider.showCandidatePort) { this._showCandidatePort = provider.showCandidatePort; + await this._proxy.$setCandidateFilter(); } if (provider.tunnelFactory) { this._forwardPortProvider = provider.tunnelFactory; - await this._proxy.$setTunnelProvider(); + await this._proxy.$setTunnelProvider(provider.tunnelFeatures ?? { + elevation: false, + public: false + }); } } else { this._forwardPortProvider = undefined; } - await this._proxy.$tunnelServiceReady(); return toDisposable(() => { this._forwardPortProvider = undefined; }); @@ -110,7 +223,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe if (silent) { hostMap.get(remote.port)!.disposeListener.dispose(); } - hostMap.get(remote.port)!.tunnel.dispose(); + await hostMap.get(remote.port)!.tunnel.dispose(); hostMap.delete(remote.port); } } @@ -120,31 +233,42 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe this._onDidChangeTunnels.fire(); } - $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise | undefined { + async $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise { if (this._forwardPortProvider) { - const providedPort = this._forwardPortProvider(tunnelOptions, tunnelCreationOptions); - if (providedPort !== undefined) { - return asPromise(() => providedPort).then(tunnel => { + try { + this.logService.trace('$forwardPort: Getting tunnel from provider.'); + const providedPort = this._forwardPortProvider(tunnelOptions, tunnelCreationOptions); + this.logService.trace('$forwardPort: Got tunnel promise from provider.'); + if (providedPort !== undefined) { + const tunnel = await providedPort; + this.logService.trace('$forwardPort: Successfully awaited tunnel from provider.'); if (!this._extensionTunnels.has(tunnelOptions.remoteAddress.host)) { this._extensionTunnels.set(tunnelOptions.remoteAddress.host, new Map()); } const disposeListener = this._register(tunnel.onDidDispose(() => this._proxy.$closeTunnel(tunnel.remoteAddress))); this._extensionTunnels.get(tunnelOptions.remoteAddress.host)!.set(tunnelOptions.remoteAddress.port, { tunnel, disposeListener }); - return Promise.resolve(TunnelDto.fromApiTunnel(tunnel)); - }); + return TunnelDto.fromApiTunnel(tunnel); + } else { + this.logService.trace('$forwardPort: Tunnel is undefined'); + } + } catch (e) { + this.logService.trace('$forwardPort: tunnel provider error'); } } return undefined; } + async $applyCandidateFilter(candidates: CandidatePort[]): Promise { + const filter = await Promise.all(candidates.map(candidate => this._showCandidatePort(candidate.host, candidate.port, candidate.detail))); + return candidates.filter((candidate, index) => filter[index]); + } - async findCandidatePorts(): Promise<{ host: string, port: number, detail: string }[]> { - const ports: { host: string, port: number, detail: string }[] = []; + async findCandidatePorts(): Promise { let tcp: string = ''; let tcp6: string = ''; try { - tcp = fs.readFileSync('/proc/net/tcp', 'utf8'); - tcp6 = fs.readFileSync('/proc/net/tcp6', 'utf8'); + tcp = await pfs.readFile('/proc/net/tcp', 'utf8'); + tcp6 = await pfs.readFile('/proc/net/tcp6', 'utf8'); } catch (e) { // File reading error. No additional handling needed. } @@ -154,105 +278,24 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe }); })); - const procChildren = fs.readdirSync('/proc'); - const processes: { pid: number, cwd: string, cmd: string }[] = []; + const procChildren = await pfs.readdir('/proc'); + const processes: { + pid: number, cwd: string, cmd: string + }[] = []; for (let childName of procChildren) { try { const pid: number = Number(childName); const childUri = resources.joinPath(URI.file('/proc'), childName); - const childStat = fs.statSync(childUri.fsPath); + const childStat = await pfs.stat(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'); + const cwd = await promisify(fs.readlink)(resources.joinPath(childUri, 'cwd').fsPath); + const cmd = await pfs.readFile(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8'); processes.push({ pid, cwd, cmd }); } } catch (e) { // } } - - const connections: { socket: number, ip: string, port: number }[] = this.loadListeningPorts(tcp, tcp6); - const sockets = this.getSockets(procSockets); - - const socketMap = sockets.reduce((m, socket) => { - m[socket.socket] = socket; - return m; - }, {} as Record); - const processMap = processes.reduce((m, process) => { - m[process.pid] = process; - return m; - }, {} as Record); - - connections.filter((connection => socketMap[connection.socket])).forEach(({ socket, ip, port }) => { - const command = processMap[socketMap[socket].pid].cmd; - if (!command.match(/.*\.vscode-server-[a-zA-Z]+\/bin.*/) && (command.indexOf('out/vs/server/main.js') === -1)) { - ports.push({ host: ip, port, detail: processMap[socketMap[socket].pid].cmd }); - } - }); - - return ports; - } - - private getSockets(stdout: string): { pid: number, socket: number }[] { - const lines = stdout.trim().split('\n'); - const mapped: { pid: number, socket: number }[] = []; - lines.forEach(line => { - const match = /\/proc\/(\d+)\/fd\/\d+ -> socket:\[(\d+)\]/.exec(line)!; - if (match && match.length >= 3) { - mapped.push({ - pid: parseInt(match[1], 10), - socket: parseInt(match[2], 10) - }); - } - }); - return mapped; - } - - private loadListeningPorts(...stdouts: string[]): { socket: number, ip: string, port: number }[] { - const table = ([] as Record[]).concat(...stdouts.map(this.loadConnectionTable)); - return [ - ...new Map( - table.filter(row => row.st === '0A') - .map(row => { - const address = row.local_address.split(':'); - return { - socket: parseInt(row.inode, 10), - ip: this.parseIpAddress(address[0]), - port: parseInt(address[1], 16) - }; - }).map(port => [port.ip + ':' + port.port, port]) - ).values() - ]; - } - - private parseIpAddress(hex: string): string { - let 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; - } - - private loadConnectionTable(stdout: string): Record[] { - const lines = stdout.trim().split('\n'); - const names = lines.shift()!.trim().split(/\s+/) - .filter(name => name !== 'rx_queue' && name !== 'tm->when'); - const table = lines.map(line => line.trim().split(/\s+/).reduce((obj, value, i) => { - obj[names[i] || i] = value; - return obj; - }, {} as Record)); - return table; + return findPorts(tcp, tcp6, procSockets, processes); } } diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts index 021af6e0..6ebbb92e 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts @@ -8,10 +8,38 @@ import { ExtensionActivationTimesBuilder } from 'vs/workbench/api/common/extHost import { AbstractExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { URI } from 'vs/base/common/uri'; import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes'; import { timeout } from 'vs/base/common/async'; +namespace TrustedFunction { + + // workaround a chrome issue not allowing to create new functions + // see https://github.com/w3c/webappsec-trusted-types/wiki/Trusted-Types-for-function-constructor + const ttpTrustedFunction = self.trustedTypes?.createPolicy('TrustedFunctionWorkaround', { + createScript: (_, ...args: string[]) => { + args.forEach((arg) => { + if (!self.trustedTypes?.isScript(arg)) { + throw new Error('TrustedScripts only, please'); + } + }); + // NOTE: This is insecure without parsing the arguments and body, + // Malicious inputs can escape the function body and execute immediately! + const fnArgs = args.slice(0, -1).join(','); + const fnBody = args.pop()!.toString(); + const body = `(function anonymous(${fnArgs}) {\n${fnBody}\n})`; + return body; + } + }); + + export function create(...args: string[]): Function { + if (!ttpTrustedFunction) { + return new Function(...args); + } + return self.eval(ttpTrustedFunction.createScript('', ...args) as unknown as string); + } +} + class WorkerRequireInterceptor extends RequireInterceptor { _installInterceptor() { } @@ -35,6 +63,8 @@ class WorkerRequireInterceptor extends RequireInterceptor { export class ExtHostExtensionService extends AbstractExtHostExtensionService { readonly extensionRuntime = ExtensionRuntime.Webworker; + private static _ttpExtensionScripts = self.trustedTypes?.createPolicy('ExtensionScripts', { createScript: source => source }); + private _fakeModules?: WorkerRequireInterceptor; protected async _beforeAlmostReadyToRunExtensions(): Promise { @@ -42,6 +72,8 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { const apiFactory = this._instaService.invokeFunction(createApiFactoryAndRegisterActors); this._fakeModules = this._instaService.createInstance(WorkerRequireInterceptor, apiFactory, this._registry); await this._fakeModules.install(); + performance.mark('code/extHost/didInitAPI'); + await this._waitForDebuggerAttachment(); } @@ -49,10 +81,16 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { return extensionDescription.browser; } - protected async _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { + protected async _loadCommonJSModule(extensionId: ExtensionIdentifier | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { module = module.with({ path: ensureSuffix(module.path, '.js') }); + if (extensionId) { + performance.mark(`code/extHost/willFetchExtensionCode/${extensionId.value}`); + } const response = await fetch(module.toString(true)); + if (extensionId) { + performance.mark(`code/extHost/didFetchExtensionCode/${extensionId.value}`); + } if (response.status !== 200) { throw new Error(response.statusText); @@ -63,7 +101,25 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { // Here we append #vscode-extension to serve as a marker, such that source maps // can be adjusted for the extra wrapping function. const sourceURL = `${module.toString(true)}#vscode-extension`; - const initFn = new Function('module', 'exports', 'require', `${source}\n//# sourceURL=${sourceURL}`); + const fullSource = `${source}\n//# sourceURL=${sourceURL}`; + let initFn: Function; + try { + initFn = TrustedFunction.create( + ExtHostExtensionService._ttpExtensionScripts?.createScript('module') as unknown as string ?? 'module', + ExtHostExtensionService._ttpExtensionScripts?.createScript('exports') as unknown as string ?? 'exports', + ExtHostExtensionService._ttpExtensionScripts?.createScript('require') as unknown as string ?? 'require', + ExtHostExtensionService._ttpExtensionScripts?.createScript(fullSource) as unknown as string ?? fullSource + ); + } catch (err) { + if (extensionId) { + console.error(`Loading code for extension ${extensionId.value} failed: ${err.message}`); + } else { + console.error(`Loading code failed: ${err.message}`); + } + console.error(`${module.toString(true)}${typeof err.line === 'number' ? ` line ${err.line}` : ''}${typeof err.column === 'number' ? ` column ${err.column}` : ''}`); + console.error(err); + throw err; + } // define commonjs globals: `module`, `exports`, and `require` const _exports = {}; @@ -78,9 +134,15 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { try { activationTimesBuilder.codeLoadingStart(); + if (extensionId) { + performance.mark(`code/extHost/willLoadExtensionCode/${extensionId.value}`); + } initFn(_module, _exports, _require); return (_module.exports !== _exports ? _module.exports : _exports); } finally { + if (extensionId) { + performance.mark(`code/extHost/didLoadExtensionCode/${extensionId.value}`); + } activationTimesBuilder.codeLoadingStop(); } } diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index cdd2cdfc..3515af94 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -8,25 +8,21 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { Action } from 'vs/base/common/actions'; import { SyncActionDescriptor, MenuId, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as WorkbenchExtensions, CATEGORIES } from 'vs/workbench/common/actions'; -import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; -import { IEditorGroupsService, GroupOrientation } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { getMenuBarVisibility } from 'vs/platform/windows/common/windows'; import { isWindows, isLinux, isWeb } from 'vs/base/common/platform'; import { IsMacNativeContext } from 'vs/platform/contextkey/common/contextkeys'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { InEditorZenModeContext, IsCenteredLayoutContext, EditorAreaVisibleContext } from 'vs/workbench/common/editor'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SideBarVisibleContext } from 'vs/workbench/common/viewlet'; -import { IViewDescriptorService, IViewsService, FocusedViewContext, ViewContainerLocation, IViewDescriptor } from 'vs/workbench/common/views'; +import { IViewDescriptorService, IViewsService, FocusedViewContext, ViewContainerLocation, IViewDescriptor, ViewContainerLocationToString } from 'vs/workbench/common/views'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IActivityBarService } from 'vs/workbench/services/activityBar/browser/activityBarService'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { Codicon } from 'vs/base/common/codicons'; const registry = Registry.as(WorkbenchExtensions.WorkbenchActions); @@ -125,54 +121,6 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { order: 3 }); -// --- Toggle Editor Layout - -export class ToggleEditorLayoutAction extends Action { - - static readonly ID = 'workbench.action.toggleEditorGroupLayout'; - static readonly LABEL = nls.localize('flipLayout', "Toggle Vertical/Horizontal Editor Layout"); - - private readonly toDispose = this._register(new DisposableStore()); - - constructor( - id: string, - label: string, - @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService - ) { - super(id, label); - - this.class = Codicon.editorLayout.classNames; - this.updateEnablement(); - - this.registerListeners(); - } - - private registerListeners(): void { - this.toDispose.add(this.editorGroupService.onDidAddGroup(() => this.updateEnablement())); - this.toDispose.add(this.editorGroupService.onDidRemoveGroup(() => this.updateEnablement())); - } - - private updateEnablement(): void { - this.enabled = this.editorGroupService.count > 1; - } - - async run(): Promise { - const newOrientation = (this.editorGroupService.orientation === GroupOrientation.VERTICAL) ? GroupOrientation.HORIZONTAL : GroupOrientation.VERTICAL; - this.editorGroupService.setGroupOrientation(newOrientation); - } -} - -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleEditorLayoutAction, { primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_0, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_0 } }), 'View: Toggle Vertical/Horizontal Editor Layout', CATEGORIES.View.value); - -MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { - group: 'z_flip', - command: { - id: ToggleEditorLayoutAction.ID, - title: nls.localize({ key: 'miToggleEditorLayout', comment: ['&& denotes a mnemonic'] }, "Flip &&Layout") - }, - order: 1 -}); - // --- Toggle Sidebar Position export class ToggleSidebarPositionAction extends Action { @@ -203,7 +151,64 @@ export class ToggleSidebarPositionAction extends Action { } } -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleSidebarPositionAction), 'View: Toggle Side Bar Position', CATEGORIES.View.value); +registerAction2(class extends Action2 { + constructor() { + super({ + id: ToggleSidebarPositionAction.ID, + title: { value: nls.localize('toggleSidebarPosition', "Toggle Side Bar Position"), original: 'Toggle Side Bar Position' }, + category: CATEGORIES.View, + f1: true + }); + } + run(accessor: ServicesAccessor) { + accessor.get(IInstantiationService).createInstance(ToggleSidebarPositionAction, ToggleSidebarPositionAction.ID, ToggleSidebarPositionAction.LABEL).run(); + } +}); +MenuRegistry.appendMenuItems([{ + id: MenuId.ViewContainerTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: ToggleSidebarPositionAction.ID, + title: nls.localize('move sidebar right', "Move Side Bar Right") + }, + when: ContextKeyExpr.and(ContextKeyExpr.notEquals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), + order: 1 + } +}, { + id: MenuId.ViewTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: ToggleSidebarPositionAction.ID, + title: nls.localize('move sidebar right', "Move Side Bar Right") + }, + when: ContextKeyExpr.and(ContextKeyExpr.notEquals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), + order: 1 + } +}, { + id: MenuId.ViewContainerTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: ToggleSidebarPositionAction.ID, + title: nls.localize('move sidebar left', "Move Side Bar Left") + }, + when: ContextKeyExpr.and(ContextKeyExpr.equals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), + order: 1 + } +}, { + id: MenuId.ViewTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: ToggleSidebarPositionAction.ID, + title: nls.localize('move sidebar left', "Move Side Bar Left") + }, + when: ContextKeyExpr.and(ContextKeyExpr.equals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), + order: 1 + } +}]); MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { group: '3_workbench_layout_move', @@ -256,27 +261,6 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { order: 5 }); -export class ToggleSidebarVisibilityAction extends Action { - - static readonly ID = 'workbench.action.toggleSidebarVisibility'; - static readonly LABEL = nls.localize('toggleSidebar', "Toggle Side Bar Visibility"); - - constructor( - id: string, - label: string, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService - ) { - super(id, label); - } - - async run(): Promise { - const hideSidebar = this.layoutService.isVisible(Parts.SIDEBAR_PART); - this.layoutService.setSideBarHidden(hideSidebar); - } -} - -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleSidebarVisibilityAction, { primary: KeyMod.CtrlCmd | KeyCode.KEY_B }), 'View: Toggle Side Bar Visibility', CATEGORIES.View.value); - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { group: '2_appearance', title: nls.localize({ key: 'miAppearance', comment: ['&& denotes a mnemonic'] }, "&&Appearance"), @@ -284,10 +268,53 @@ MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { order: 1 }); +export const TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID = 'workbench.action.toggleSidebarVisibility'; +registerAction2(class extends Action2 { + constructor() { + super({ + id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID, + title: { value: nls.localize('toggleSidebar', "Toggle Side Bar Visibility"), original: 'Toggle Side Bar Visibility' }, + category: CATEGORIES.View, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KEY_B + } + }); + } + run(accessor: ServicesAccessor) { + const layoutService = accessor.get(IWorkbenchLayoutService); + layoutService.setSideBarHidden(layoutService.isVisible(Parts.SIDEBAR_PART)); + } +}); +MenuRegistry.appendMenuItems([{ + id: MenuId.ViewContainerTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID, + title: nls.localize('compositePart.hideSideBarLabel', "Hide Side Bar"), + }, + when: ContextKeyExpr.and(SideBarVisibleContext, ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), + order: 2 + } +}, { + id: MenuId.ViewTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID, + title: nls.localize('compositePart.hideSideBarLabel', "Hide Side Bar"), + }, + when: ContextKeyExpr.and(SideBarVisibleContext, ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), + order: 2 + } +}]); + MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { group: '2_workbench_layout', command: { - id: ToggleSidebarVisibilityAction.ID, + id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID, title: nls.localize({ key: 'miShowSidebar', comment: ['&& denotes a mnemonic'] }, "Show &&Side Bar"), toggled: SideBarVisibleContext }, @@ -413,32 +440,16 @@ export class ToggleMenuBarAction extends Action { static readonly ID = 'workbench.action.toggleMenuBar'; static readonly LABEL = nls.localize('toggleMenuBar', "Toggle Menu Bar"); - private static readonly menuBarVisibilityKey = 'window.menuBarVisibility'; - constructor( id: string, label: string, - @IConfigurationService private readonly configurationService: IConfigurationService + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService ) { super(id, label); } - run(): Promise { - let currentVisibilityValue = getMenuBarVisibility(this.configurationService); - if (typeof currentVisibilityValue !== 'string') { - currentVisibilityValue = 'default'; - } - - let newVisibilityValue: string; - if (currentVisibilityValue === 'visible' || currentVisibilityValue === 'default') { - newVisibilityValue = 'toggle'; - } else if (currentVisibilityValue === 'compact') { - newVisibilityValue = 'hidden'; - } else { - newVisibilityValue = (isWeb && currentVisibilityValue === 'hidden') ? 'compact' : 'default'; - } - - return this.configurationService.updateValue(ToggleMenuBarAction.menuBarVisibilityKey, newVisibilityValue, ConfigurationTarget.USER); + async run(): Promise { + this.layoutService.toggleMenuBar(); } } @@ -479,19 +490,18 @@ export class ResetViewLocationsAction extends Action { registry.registerWorkbenchAction(SyncActionDescriptor.from(ResetViewLocationsAction), 'View: Reset View Locations', CATEGORIES.View.value); // --- Toggle View with Command -export abstract class ToggleViewAction extends Action { +export class ToggleViewAction extends Action { constructor( id: string, label: string, private readonly viewId: string, - protected viewsService: IViewsService, - protected viewDescriptorService: IViewDescriptorService, - protected contextKeyService: IContextKeyService, - private layoutService: IWorkbenchLayoutService, - cssClass?: string + @IViewsService protected viewsService: IViewsService, + @IViewDescriptorService protected viewDescriptorService: IViewDescriptorService, + @IContextKeyService protected contextKeyService: IContextKeyService, + @IWorkbenchLayoutService private layoutService: IWorkbenchLayoutService, ) { - super(id, label, cssClass); + super(id, label); } async run(): Promise { diff --git a/src/vs/workbench/browser/actions/navigationActions.ts b/src/vs/workbench/browser/actions/navigationActions.ts index 5833c329..20accba7 100644 --- a/src/vs/workbench/browser/actions/navigationActions.ts +++ b/src/vs/workbench/browser/actions/navigationActions.ts @@ -17,7 +17,6 @@ import { IWorkbenchActionRegistry, Extensions, CATEGORIES } from 'vs/workbench/c import { Direction } from 'vs/base/browser/ui/grid/grid'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { isAncestor } from 'vs/base/browser/dom'; abstract class BaseNavigationAction extends Action { @@ -215,7 +214,8 @@ function findVisibleNeighbour(layoutService: IWorkbenchLayoutService, part: Part } function focusNextOrPreviousPart(layoutService: IWorkbenchLayoutService, editorService: IEditorService, next: boolean): void { - const currentlyFocusedPart = isActiveElementInNotebookEditor(editorService) ? Parts.EDITOR_PART : layoutService.hasFocus(Parts.EDITOR_PART) ? Parts.EDITOR_PART : layoutService.hasFocus(Parts.ACTIVITYBAR_PART) ? Parts.ACTIVITYBAR_PART : + const editorFocused = editorService.activeEditorPane?.hasFocus(); + const currentlyFocusedPart = editorFocused ? Parts.EDITOR_PART : layoutService.hasFocus(Parts.ACTIVITYBAR_PART) ? Parts.ACTIVITYBAR_PART : layoutService.hasFocus(Parts.STATUSBAR_PART) ? Parts.STATUSBAR_PART : layoutService.hasFocus(Parts.SIDEBAR_PART) ? Parts.SIDEBAR_PART : layoutService.hasFocus(Parts.PANEL_PART) ? Parts.PANEL_PART : undefined; let partToFocus = Parts.EDITOR_PART; if (currentlyFocusedPart) { @@ -225,17 +225,6 @@ function focusNextOrPreviousPart(layoutService: IWorkbenchLayoutService, editorS layoutService.focusPart(partToFocus); } -function isActiveElementInNotebookEditor(editorService: IEditorService): boolean { - const activeEditorPane = editorService.activeEditorPane as unknown as { isNotebookEditor?: boolean } | undefined; - if (activeEditorPane?.isNotebookEditor) { - const control = editorService.activeEditorPane?.getControl() as { getDomNode(): HTMLElement; getOverflowContainerDomNode(): HTMLElement; }; - const activeElement = document.activeElement; - return isAncestor(activeElement, control.getDomNode()) || isAncestor(activeElement, control.getOverflowContainerDomNode()); - } - - return false; -} - export class FocusNextPart extends Action { static readonly ID = 'workbench.action.focusNextPart'; static readonly LABEL = nls.localize('focusNextPart', "Focus Next Part"); diff --git a/src/vs/workbench/browser/actions/textInputActions.ts b/src/vs/workbench/browser/actions/textInputActions.ts index 8b63f052..606099d2 100644 --- a/src/vs/workbench/browser/actions/textInputActions.ts +++ b/src/vs/workbench/browser/actions/textInputActions.ts @@ -76,23 +76,26 @@ export class TextInputActionsProvider extends Disposable implements IWorkbenchCo // Context menu support in input/textarea this.layoutService.container.addEventListener('contextmenu', e => this.onContextMenu(e)); - } private onContextMenu(e: MouseEvent): void { - if (e.target instanceof HTMLElement) { - const target = e.target; - if (target.nodeName && (target.nodeName.toLowerCase() === 'input' || target.nodeName.toLowerCase() === 'textarea')) { - EventHelper.stop(e, true); - - this.contextMenuService.showContextMenu({ - getAnchor: () => e, - getActions: () => this.textInputActions, - getActionsContext: () => target, - onHide: () => target.focus() // fixes https://github.com/microsoft/vscode/issues/52948 - }); - } + if (e.defaultPrevented) { + return; // make sure to not show these actions by accident if component indicated to prevent } + + const target = e.target; + if (!(target instanceof HTMLElement) || (target.nodeName.toLowerCase() !== 'input' && target.nodeName.toLowerCase() !== 'textarea')) { + return; // only for inputs or textareas + } + + EventHelper.stop(e, true); + + this.contextMenuService.showContextMenu({ + getAnchor: () => e, + getActions: () => this.textInputActions, + getActionsContext: () => target, + onHide: () => target.focus() // fixes https://github.com/microsoft/vscode/issues/52948 + }); } } diff --git a/src/vs/workbench/browser/actions/windowActions.ts b/src/vs/workbench/browser/actions/windowActions.ts index e5fdcdbc..92652787 100644 --- a/src/vs/workbench/browser/actions/windowActions.ts +++ b/src/vs/workbench/browser/actions/windowActions.ts @@ -33,7 +33,7 @@ import { ResourceMap } from 'vs/base/common/map'; import { Codicon } from 'vs/base/common/codicons'; import { isHTMLElement } from 'vs/base/browser/dom'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export const inRecentFilesPickerContextKey = 'inRecentFilesPicker'; @@ -49,12 +49,17 @@ abstract class BaseOpenRecentAction extends Action { tooltip: nls.localize('remove', "Remove from Recently Opened") }; - private readonly dirtyRecentlyOpened: IQuickInputButton = { + private readonly dirtyRecentlyOpenedFolder: IQuickInputButton = { iconClass: 'dirty-workspace ' + Codicon.closeDirty.classNames, - tooltip: nls.localize('dirtyRecentlyOpened', "Workspace With Dirty Files"), + tooltip: nls.localize('dirtyRecentlyOpenedFolder', "Folder With Unsaved Files"), alwaysVisible: true }; + private readonly dirtyRecentlyOpenedWorkspace: IQuickInputButton = { + ...this.dirtyRecentlyOpenedFolder, + tooltip: nls.localize('dirtyRecentlyOpenedWorkspace', "Workspace With Unsaved Files"), + }; + constructor( id: string, label: string, @@ -77,7 +82,9 @@ abstract class BaseOpenRecentAction extends Action { const recentlyOpened = await this.workspacesService.getRecentlyOpened(); const dirtyWorkspacesAndFolders = await this.workspacesService.getDirtyWorkspaces(); - // Identify all folders and workspaces with dirty files + let hasWorkspaces = false; + + // Identify all folders and workspaces with unsaved files const dirtyFolders = new ResourceMap(); const dirtyWorkspaces = new ResourceMap(); for (const dirtyWorkspace of dirtyWorkspacesAndFolders) { @@ -85,6 +92,7 @@ abstract class BaseOpenRecentAction extends Action { dirtyFolders.set(dirtyWorkspace, true); } else { dirtyWorkspaces.set(dirtyWorkspace.configPath, dirtyWorkspace); + hasWorkspaces = true; } } @@ -96,6 +104,7 @@ abstract class BaseOpenRecentAction extends Action { recentFolders.set(recent.folderUri, true); } else { recentWorkspaces.set(recent.workspace.configPath, recent.workspace); + hasWorkspaces = true; } } @@ -124,7 +133,7 @@ abstract class BaseOpenRecentAction extends Action { let keyMods: IKeyMods | undefined; - const workspaceSeparator: IQuickPickSeparator = { type: 'separator', label: nls.localize('workspaces', "workspaces") }; + const workspaceSeparator: IQuickPickSeparator = { type: 'separator', label: hasWorkspaces ? nls.localize('workspacesAndFolders', "folders & workspaces") : nls.localize('folders', "folders") }; const fileSeparator: IQuickPickSeparator = { type: 'separator', label: nls.localize('files', "files") }; const picks = [workspaceSeparator, ...workspacePicks, fileSeparator, ...filePicks]; @@ -143,13 +152,14 @@ abstract class BaseOpenRecentAction extends Action { context.removeItem(); } - // Dirty Workspace - else if (context.button === this.dirtyRecentlyOpened) { + // Dirty Folder/Workspace + else if (context.button === this.dirtyRecentlyOpenedFolder || context.button === this.dirtyRecentlyOpenedWorkspace) { + const isDirtyWorkspace = context.button === this.dirtyRecentlyOpenedWorkspace; const result = await this.dialogService.confirm({ type: 'question', - title: nls.localize('dirtyWorkspace', "Workspace with Dirty Files"), - message: nls.localize('dirtyWorkspaceConfirm', "Do you want to open the workspace to review the dirty files?"), - detail: nls.localize('dirtyWorkspaceConfirmDetail', "Workspaces with dirty files cannot be removed until all dirty files have been saved or reverted.") + title: isDirtyWorkspace ? nls.localize('dirtyWorkspace', "Workspace with Unsaved Files") : nls.localize('dirtyFolder', "Folder with Unsaved Files"), + message: isDirtyWorkspace ? nls.localize('dirtyWorkspaceConfirm', "Do you want to open the workspace to review the unsaved files?") : nls.localize('dirtyFolderConfirm', "Do you want to open the folder to review the unsaved files?"), + detail: isDirtyWorkspace ? nls.localize('dirtyWorkspaceConfirmDetail', "Workspaces with unsaved files cannot be removed until all unsaved files have been saved or reverted.") : nls.localize('dirtyFolderConfirmDetail', "Folders with unsaved files cannot be removed until all unsaved files have been saved or reverted.") }); if (result.confirmed) { @@ -170,6 +180,7 @@ abstract class BaseOpenRecentAction extends Action { let iconClasses: string[]; let fullLabel: string | undefined; let resource: URI | undefined; + let isWorkspace = false; // Folder if (isRecentFolder(recent)) { @@ -185,6 +196,7 @@ abstract class BaseOpenRecentAction extends Action { iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.ROOT_FOLDER); openable = { workspaceUri: resource }; fullLabel = recent.label || this.labelService.getWorkspaceLabel(recent.workspace, { verbose: true }); + isWorkspace = true; } // File @@ -200,9 +212,9 @@ abstract class BaseOpenRecentAction extends Action { return { iconClasses, label: name, - ariaLabel: isDirty ? nls.localize('recentDirtyAriaLabel', "{0}, dirty workspace", name) : name, + ariaLabel: isDirty ? isWorkspace ? nls.localize('recentDirtyWorkspaceAriaLabel', "{0}, workspace with unsaved changes", name) : nls.localize('recentDirtyFolderAriaLabel', "{0}, folder with unsaved changes", name) : name, description: parentPath, - buttons: isDirty ? [this.dirtyRecentlyOpened] : [this.removeFromRecentlyOpened], + buttons: isDirty ? [isWorkspace ? this.dirtyRecentlyOpenedWorkspace : this.dirtyRecentlyOpenedFolder] : [this.removeFromRecentlyOpened], openable, resource }; @@ -405,7 +417,7 @@ CommandsRegistry.registerCommand('workbench.action.toggleConfirmBeforeClose', ac const configurationService = accessor.get(IConfigurationService); const setting = configurationService.inspect<'always' | 'keyboardOnly' | 'never'>('window.confirmBeforeClose').userValue; - return configurationService.updateValue('window.confirmBeforeClose', setting === 'never' ? 'keyboardOnly' : 'never', ConfigurationTarget.USER); + return configurationService.updateValue('window.confirmBeforeClose', setting === 'never' ? 'keyboardOnly' : 'never'); }); // --- Menu Registration diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts index b7357376..ea67ce4c 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -114,7 +114,7 @@ export class CloseWorkspaceAction extends Action { async run(): Promise { if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { - this.notificationService.info(nls.localize('noWorkspaceOpened', "There is currently no workspace opened in this instance to close.")); + this.notificationService.info(nls.localize('noWorkspaceOrFolderOpened', "There is currently no workspace or folder opened in this instance to close.")); return; } @@ -225,7 +225,7 @@ export class SaveWorkspaceAsAction extends Action { export class DuplicateWorkspaceInNewWindowAction extends Action { static readonly ID = 'workbench.action.duplicateWorkspaceInNewWindow'; - static readonly LABEL = nls.localize('duplicateWorkspaceInNewWindow', "Duplicate Workspace in New Window"); + static readonly LABEL = nls.localize('duplicateWorkspaceInNewWindow', "Duplicate As Workspace in New Window"); constructor( id: string, @@ -259,7 +259,7 @@ registry.registerWorkbenchAction(SyncActionDescriptor.from(AddRootFolderAction), registry.registerWorkbenchAction(SyncActionDescriptor.from(GlobalRemoveRootFolderAction), 'Workspaces: Remove Folder from Workspace...', workspacesCategory); registry.registerWorkbenchAction(SyncActionDescriptor.from(CloseWorkspaceAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_F) }), 'Workspaces: Close Workspace', workspacesCategory, EmptyWorkspaceSupportContext); registry.registerWorkbenchAction(SyncActionDescriptor.from(SaveWorkspaceAsAction), 'Workspaces: Save Workspace As...', workspacesCategory, EmptyWorkspaceSupportContext); -registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateWorkspaceInNewWindowAction), 'Workspaces: Duplicate Workspace in New Window', workspacesCategory); +registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateWorkspaceInNewWindowAction), 'Workspaces: Duplicate As Workspace in New Window', workspacesCategory); // --- Menu Registration diff --git a/src/vs/workbench/browser/parts/editor/editorWidgets.ts b/src/vs/workbench/browser/codeeditor.ts similarity index 60% rename from src/vs/workbench/browser/parts/editor/editorWidgets.ts rename to src/vs/workbench/browser/codeeditor.ts index 02601bc9..7bcac2ca 100644 --- a/src/vs/workbench/browser/parts/editor/editorWidgets.ts +++ b/src/vs/workbench/browser/codeeditor.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Widget } from 'vs/base/browser/ui/widget'; -import { IOverlayWidget, ICodeEditor, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; +import { IOverlayWidget, ICodeEditor, IOverlayWidgetPosition, OverlayWidgetPositionPreference, isCodeEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser'; import { Emitter } from 'vs/base/common/event'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -15,11 +15,121 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; -import { Disposable, dispose } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { isEqual } from 'vs/base/common/resources'; import { IFileService } from 'vs/platform/files/common/files'; +import { URI } from 'vs/base/common/uri'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IRange } from 'vs/editor/common/core/range'; +import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { TrackedRangeStickiness, IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; + +export interface IRangeHighlightDecoration { + resource: URI; + range: IRange; + isWholeLine?: boolean; +} + +export class RangeHighlightDecorations extends Disposable { + + private readonly _onHighlightRemoved = this._register(new Emitter()); + readonly onHighlightRemoved = this._onHighlightRemoved.event; + + private rangeHighlightDecorationId: string | null = null; + private editor: ICodeEditor | null = null; + private readonly editorDisposables = this._register(new DisposableStore()); + + constructor(@IEditorService private readonly editorService: IEditorService) { + super(); + } + + removeHighlightRange() { + if (this.editor && this.editor.getModel() && this.rangeHighlightDecorationId) { + this.editor.deltaDecorations([this.rangeHighlightDecorationId], []); + this._onHighlightRemoved.fire(); + } + + this.rangeHighlightDecorationId = null; + } + + highlightRange(range: IRangeHighlightDecoration, editor?: any) { + editor = editor ?? this.getEditor(range); + if (isCodeEditor(editor)) { + this.doHighlightRange(editor, range); + } else if (isCompositeEditor(editor) && isCodeEditor(editor.activeCodeEditor)) { + this.doHighlightRange(editor.activeCodeEditor, range); + } + } + + private doHighlightRange(editor: ICodeEditor, selectionRange: IRangeHighlightDecoration) { + this.removeHighlightRange(); + + editor.changeDecorations((changeAccessor: IModelDecorationsChangeAccessor) => { + this.rangeHighlightDecorationId = changeAccessor.addDecoration(selectionRange.range, this.createRangeHighlightDecoration(selectionRange.isWholeLine)); + }); + + this.setEditor(editor); + } + + private getEditor(resourceRange: IRangeHighlightDecoration): ICodeEditor | undefined { + const activeEditor = this.editorService.activeEditor; + const resource = activeEditor && activeEditor.resource; + if (resource && isEqual(resource, resourceRange.resource)) { + return this.editorService.activeTextEditorControl as ICodeEditor; + } + + return undefined; + } + + private setEditor(editor: ICodeEditor) { + if (this.editor !== editor) { + this.editorDisposables.clear(); + this.editor = editor; + this.editorDisposables.add(this.editor.onDidChangeCursorPosition((e: ICursorPositionChangedEvent) => { + if ( + e.reason === CursorChangeReason.NotSet + || e.reason === CursorChangeReason.Explicit + || e.reason === CursorChangeReason.Undo + || e.reason === CursorChangeReason.Redo + ) { + this.removeHighlightRange(); + } + })); + this.editorDisposables.add(this.editor.onDidChangeModel(() => { this.removeHighlightRange(); })); + this.editorDisposables.add(this.editor.onDidDispose(() => { + this.removeHighlightRange(); + this.editor = null; + })); + } + } + + private static readonly _WHOLE_LINE_RANGE_HIGHLIGHT = ModelDecorationOptions.register({ + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + className: 'rangeHighlight', + isWholeLine: true + }); + + private static readonly _RANGE_HIGHLIGHT = ModelDecorationOptions.register({ + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + className: 'rangeHighlight' + }); + + private createRangeHighlightDecoration(isWholeLine: boolean = true): ModelDecorationOptions { + return (isWholeLine ? RangeHighlightDecorations._WHOLE_LINE_RANGE_HIGHLIGHT : RangeHighlightDecorations._RANGE_HIGHLIGHT); + } + + dispose() { + super.dispose(); + + if (this.editor && this.editor.getModel()) { + this.removeHighlightRange(); + this.editor = null; + } + } +} export class FloatingClickWidget extends Widget implements IOverlayWidget { diff --git a/src/vs/workbench/browser/composite.ts b/src/vs/workbench/browser/composite.ts index 16f7cf46..c0087e85 100644 --- a/src/vs/workbench/browser/composite.ts +++ b/src/vs/workbench/browser/composite.ts @@ -56,15 +56,26 @@ export abstract class Composite extends Component implements IComposite { return this._onDidBlur.event; } + private _hasFocus = false; + hasFocus(): boolean { + return this._hasFocus; + } + private registerFocusTrackEvents(): { onDidFocus: Emitter, onDidBlur: Emitter } { const container = assertIsDefined(this.getContainer()); const focusTracker = this._register(trackFocus(container)); const onDidFocus = this._onDidFocus = this._register(new Emitter()); - this._register(focusTracker.onDidFocus(() => onDidFocus.fire())); + this._register(focusTracker.onDidFocus(() => { + this._hasFocus = true; + onDidFocus.fire(); + })); const onDidBlur = this._onDidBlur = this._register(new Emitter()); - this._register(focusTracker.onDidBlur(() => onDidBlur.fire())); + this._register(focusTracker.onDidBlur(() => { + this._hasFocus = false; + onDidBlur.fire(); + })); return { onDidFocus, onDidBlur }; } @@ -234,7 +245,6 @@ export abstract class CompositeDescriptor { readonly cssClass?: string, readonly order?: number, readonly requestedIndex?: number, - readonly keybindingId?: string, ) { } instantiate(instantiationService: IInstantiationService): T { diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index 10e7b53e..833ec221 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -17,7 +17,7 @@ import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/ import { SideBarVisibleContext } from 'vs/workbench/common/viewlet'; import { IWorkbenchLayoutService, Parts, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { PanelPositionContext } from 'vs/workbench/common/panel'; +import { PanelMaximizedContext, PanelPositionContext, PanelVisibleContext } from 'vs/workbench/common/panel'; import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { isNative } from 'vs/base/common/platform'; @@ -64,6 +64,8 @@ export class WorkbenchContextKeysHandler extends Disposable { private sideBarVisibleContext: IContextKey; private editorAreaVisibleContext: IContextKey; private panelPositionContext: IContextKey; + private panelVisibleContext: IContextKey; + private panelMaximizedContext: IContextKey; constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -146,9 +148,13 @@ export class WorkbenchContextKeysHandler extends Disposable { // Sidebar this.sideBarVisibleContext = SideBarVisibleContext.bindTo(this.contextKeyService); - // Panel Position + // Panel this.panelPositionContext = PanelPositionContext.bindTo(this.contextKeyService); this.panelPositionContext.set(positionToString(this.layoutService.getPanelPosition())); + this.panelVisibleContext = PanelVisibleContext.bindTo(this.contextKeyService); + this.panelVisibleContext.set(this.layoutService.isVisible(Parts.PANEL_PART)); + this.panelMaximizedContext = PanelMaximizedContext.bindTo(this.contextKeyService); + this.panelMaximizedContext.set(this.layoutService.isPanelMaximized()); this.registerListeners(); } @@ -182,7 +188,11 @@ export class WorkbenchContextKeysHandler extends Disposable { this._register(this.viewletService.onDidViewletClose(() => this.updateSideBarContextKeys())); this._register(this.viewletService.onDidViewletOpen(() => this.updateSideBarContextKeys())); - this._register(this.layoutService.onPartVisibilityChange(() => this.editorAreaVisibleContext.set(this.layoutService.isVisible(Parts.EDITOR_PART)))); + this._register(this.layoutService.onPartVisibilityChange(() => { + this.editorAreaVisibleContext.set(this.layoutService.isVisible(Parts.EDITOR_PART)); + this.panelVisibleContext.set(this.layoutService.isVisible(Parts.PANEL_PART)); + this.panelMaximizedContext.set(this.layoutService.isPanelMaximized()); + })); this._register(this.workingCopyService.onDidChangeDirty(workingCopy => this.dirtyWorkingCopiesContext.set(workingCopy.isDirty() || this.workingCopyService.hasDirty))); } diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index baab1343..962b92b7 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -737,7 +737,7 @@ export class CompositeDragAndDropObserver extends Disposable { if (callbacks.onDragEnd) { this._onDragEnd.event(e => { callbacks.onDragEnd!(e); - }); + }, this, disposableStore); } return this._register(disposableStore); } diff --git a/src/vs/workbench/browser/editor.ts b/src/vs/workbench/browser/editor.ts index 5df3e69f..fd865714 100644 --- a/src/vs/workbench/browser/editor.ts +++ b/src/vs/workbench/browser/editor.ts @@ -12,7 +12,6 @@ import { insert } from 'vs/base/common/arrays'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; export interface IEditorDescriptor { - getId(): string; getName(): string; diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 5984484d..a4729a6b 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -100,6 +100,10 @@ export const DEFAULT_LABELS_CONTAINER: IResourceLabelsContainer = { }; export class ResourceLabels extends Disposable { + + private _onDidChangeDecorations = this._register(new Emitter()); + readonly onDidChangeDecorations = this._onDidChangeDecorations.event; + private widgets: ResourceLabelWidget[] = []; private labels: IResourceLabel[] = []; @@ -148,7 +152,18 @@ export class ResourceLabels extends Disposable { })); // notify when file decoration changes - this._register(this.decorationsService.onDidChangeDecorations(e => this.widgets.forEach(widget => widget.notifyFileDecorationsChanges(e)))); + this._register(this.decorationsService.onDidChangeDecorations(e => { + let notifyDidChangeDecorations = false; + this.widgets.forEach(widget => { + if (widget.notifyFileDecorationsChanges(e)) { + notifyDidChangeDecorations = true; + } + }); + + if (notifyDidChangeDecorations) { + this._onDidChangeDecorations.fire(); + } + })); // notify when theme changes this._register(this.themeService.onDidColorThemeChange(() => this.widgets.forEach(widget => widget.notifyThemeChange()))); @@ -311,19 +326,21 @@ class ResourceLabelWidget extends IconLabel { } } - notifyFileDecorationsChanges(e: IResourceDecorationChangeEvent): void { + notifyFileDecorationsChanges(e: IResourceDecorationChangeEvent): boolean { if (!this.options) { - return; + return false; } const resource = toResource(this.label); if (!resource) { - return; + return false; } if (this.options.fileDecorations && e.affectsResource(resource)) { - this.render(false); + return this.render(false); } + + return false; } notifyExtensionsRegistered(): void { @@ -465,7 +482,7 @@ class ResourceLabelWidget extends IconLabel { this.setLabel(''); } - private render(clearIconCache: boolean): void { + private render(clearIconCache: boolean): boolean { if (this.isHidden) { if (!this.needsRedraw) { this.needsRedraw = clearIconCache ? Redraw.Full : Redraw.Basic; @@ -475,7 +492,7 @@ class ResourceLabelWidget extends IconLabel { this.needsRedraw = Redraw.Full; } - return; + return false; } if (this.label) { @@ -492,7 +509,7 @@ class ResourceLabelWidget extends IconLabel { } if (!this.label) { - return; + return false; } this.renderDisposables.clear(); @@ -558,6 +575,8 @@ class ResourceLabelWidget extends IconLabel { this.setLabel(label || '', this.label.description, iconLabelOptions); this._onDidRender.fire(); + + return true; } dispose(): void { diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 460594ee..1fe5aa62 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -78,7 +78,9 @@ enum Storage { GRID_LAYOUT = 'workbench.grid.layout', GRID_WIDTH = 'workbench.grid.width', - GRID_HEIGHT = 'workbench.grid.height' + GRID_HEIGHT = 'workbench.grid.height', + + MENU_VISIBILITY = 'window.menuBarVisibility' } enum Classes { @@ -321,9 +323,12 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi if (this.state.fullscreen && (this.state.menuBar.visibility === 'toggle' || this.state.menuBar.visibility === 'default')) { // Propagate to grid this.workbenchGrid.setViewVisible(this.titleBarPartView, this.isVisible(Parts.TITLEBAR_PART)); - - this.layout(); } + + // Move layout call to any time the menubar + // is toggled to update consumers of offset + // see issue #115267 + this.layout(); } } @@ -622,7 +627,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return this._openedDefaultEditors; } - private getInitialFilesToOpen(): { filesToOpenOrCreate?: IPath[], filesToDiff?: IPath[] } | undefined { + private getInitialFilesToOpen(): { filesToOpenOrCreate?: IPath[], filesToDiff?: IPath[]; } | undefined { const defaultLayout = this.environmentService.options?.defaultLayout; if (defaultLayout?.editors?.length && this.storageService.isNew(StorageScope.WORKSPACE)) { this._openedDefaultEditors = true; @@ -652,7 +657,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Restore editors restorePromises.push((async () => { - mark('willRestoreEditors'); + mark('code/willRestoreEditors'); // first ensure the editor part is restored await this.editorGroupService.whenRestored; @@ -669,17 +674,17 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi await this.editorService.openEditors(editors); } - mark('didRestoreEditors'); + mark('code/didRestoreEditors'); })()); // Restore default views const restoreDefaultViewsPromise = (async () => { if (this.state.views.defaults?.length) { - mark('willOpenDefaultViews'); + mark('code/willOpenDefaultViews'); - let locationsRestored: { id: string; order: number }[] = []; + let locationsRestored: { id: string; order: number; }[] = []; - const tryOpenView = (view: { id: string; order: number }): boolean => { + const tryOpenView = (view: { id: string; order: number; }): boolean => { const location = this.viewDescriptorService.getViewLocationById(view.id); if (location !== null) { const container = this.viewDescriptorService.getViewContainerByViewId(view.id); @@ -733,7 +738,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.state.panel.panelToRestore = locationsRestored[ViewContainerLocation.Panel].id; } - mark('didOpenDefaultViews'); + mark('code/didOpenDefaultViews'); } })(); restorePromises.push(restoreDefaultViewsPromise); @@ -748,14 +753,14 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return; } - mark('willRestoreViewlet'); + mark('code/willRestoreViewlet'); const viewlet = await this.viewletService.openViewlet(this.state.sideBar.viewletToRestore); if (!viewlet) { await this.viewletService.openViewlet(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id); // fallback to default viewlet as needed } - mark('didRestoreViewlet'); + mark('code/didRestoreViewlet'); })()); // Restore Panel @@ -768,14 +773,14 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return; } - mark('willRestorePanel'); + mark('code/willRestorePanel'); const panel = await this.panelService.openPanel(this.state.panel.panelToRestore!); if (!panel) { await this.panelService.openPanel(Registry.as(PanelExtensions.Panels).getDefaultPanelId()); // fallback to default panel as needed } - mark('didRestorePanel'); + mark('code/didRestorePanel'); })()); // Restore Zen Mode @@ -1128,7 +1133,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi [Parts.STATUSBAR_PART]: this.statusBarPartView }; - const fromJSON = ({ type }: { type: Parts }) => viewMap[type]; + const fromJSON = ({ type }: { type: Parts; }) => viewMap[type]; const workbenchGrid = SerializableGrid.deserialize( this.createGridDescriptor(), { fromJSON }, @@ -1410,7 +1415,29 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // If panel part becomes visible, show last active panel or default panel else if (!hidden && !this.panelService.getActivePanel()) { - const panelToOpen = this.panelService.getLastActivePanelId(); + let panelToOpen: string | undefined = this.panelService.getLastActivePanelId(); + const hasViews = (id: string): boolean => { + const viewContainer = this.viewDescriptorService.getViewContainerById(id); + if (!viewContainer) { + return false; + } + + const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); + if (!viewContainerModel) { + return false; + } + + return viewContainerModel.activeViewDescriptors.length >= 1; + }; + + // verify that the panel we try to open has views before we default to it + // otherwise fall back to any view that has views still refs #111463 + if (!panelToOpen || !hasViews(panelToOpen)) { + panelToOpen = this.viewDescriptorService + .getViewContainersByLocation(ViewContainerLocation.Panel) + .find(viewContainer => hasViews(viewContainer.id))?.id; + } + if (panelToOpen) { const focus = !skipLayout; this.panelService.openPanel(panelToOpen, focus); @@ -1529,6 +1556,24 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return this.state.menuBar.visibility; } + toggleMenuBar(): void { + let currentVisibilityValue = getMenuBarVisibility(this.configurationService); + if (typeof currentVisibilityValue !== 'string') { + currentVisibilityValue = 'default'; + } + + let newVisibilityValue: string; + if (currentVisibilityValue === 'visible' || currentVisibilityValue === 'default') { + newVisibilityValue = 'toggle'; + } else if (currentVisibilityValue === 'compact') { + newVisibilityValue = 'hidden'; + } else { + newVisibilityValue = (isWeb && currentVisibilityValue === 'hidden') ? 'compact' : 'default'; + } + + this.configurationService.updateValue(Storage.MENU_VISIBILITY, newVisibilityValue); + } + getPanelPosition(): Position { return this.state.panel.position; } diff --git a/src/vs/workbench/browser/menuActions.ts b/src/vs/workbench/browser/menuActions.ts new file mode 100644 index 00000000..7aecec66 --- /dev/null +++ b/src/vs/workbench/browser/menuActions.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IAction } from 'vs/base/common/actions'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; +import { MenuId, IMenuService, IMenu, SubmenuItemAction, IMenuActionOptions } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; + +class MenuActions extends Disposable { + + private readonly menu: IMenu; + + private _primaryActions: IAction[] = []; + get primaryActions() { return this._primaryActions; } + + private _secondaryActions: IAction[] = []; + get secondaryActions() { return this._secondaryActions; } + + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + + private disposables = this._register(new DisposableStore()); + + constructor( + menuId: MenuId, + private readonly options: IMenuActionOptions | undefined, + private readonly menuService: IMenuService, + private readonly contextKeyService: IContextKeyService + ) { + super(); + this.menu = this._register(menuService.createMenu(menuId, contextKeyService)); + this._register(this.menu.onDidChange(() => this.updateActions())); + this.updateActions(); + } + + private updateActions(): void { + this.disposables.clear(); + this._primaryActions = []; + this._secondaryActions = []; + this.disposables.add(createAndFillInActionBarActions(this.menu, this.options, { primary: this._primaryActions, secondary: this._secondaryActions })); + this.disposables.add(this.updateSubmenus([...this._primaryActions, ...this._secondaryActions], {})); + this._onDidChange.fire(); + } + + private updateSubmenus(actions: readonly IAction[], submenus: { [id: number]: IMenu }): IDisposable { + const disposables = new DisposableStore(); + for (const action of actions) { + if (action instanceof SubmenuItemAction && !submenus[action.item.submenu.id]) { + const menu = submenus[action.item.submenu.id] = disposables.add(this.menuService.createMenu(action.item.submenu, this.contextKeyService)); + disposables.add(menu.onDidChange(() => this.updateActions())); + disposables.add(this.updateSubmenus(action.actions, submenus)); + } + } + return disposables; + } +} + +export class CompositeMenuActions extends Disposable { + + private readonly menuActions: MenuActions; + private readonly contextMenuActionsDisposable = this._register(new MutableDisposable()); + + private _onDidChange = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; + + constructor( + menuId: MenuId, + private readonly contextMenuId: MenuId | undefined, + private readonly options: IMenuActionOptions | undefined, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IMenuService private readonly menuService: IMenuService, + ) { + super(); + this.menuActions = this._register(new MenuActions(menuId, this.options, menuService, contextKeyService)); + this._register(this.menuActions.onDidChange(() => this._onDidChange.fire())); + } + + getPrimaryActions(): IAction[] { + return this.menuActions.primaryActions; + } + + getSecondaryActions(): IAction[] { + return this.menuActions.secondaryActions; + } + + getContextMenuActions(): IAction[] { + const actions: IAction[] = []; + if (this.contextMenuId) { + const menu = this.menuService.createMenu(this.contextMenuId, this.contextKeyService); + this.contextMenuActionsDisposable.value = createAndFillInActionBarActions(menu, this.options, { primary: [], secondary: actions }); + menu.dispose(); + } + return actions; + } +} diff --git a/src/vs/workbench/browser/panecomposite.ts b/src/vs/workbench/browser/panecomposite.ts index 5279b52e..4a8a1144 100644 --- a/src/vs/workbench/browser/panecomposite.ts +++ b/src/vs/workbench/browser/panecomposite.ts @@ -16,13 +16,9 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { ViewPaneContainer } from './parts/views/viewPaneContainer'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; import { IAction, IActionViewItem, Separator } from 'vs/base/common/actions'; -import { ViewContainerMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; -import { MenuId } from 'vs/platform/actions/common/actions'; export class PaneComposite extends Composite implements IPaneComposite { - private menuActions: ViewContainerMenuActions; - constructor( id: string, protected readonly viewPaneContainer: ViewPaneContainer, @@ -35,8 +31,6 @@ export class PaneComposite extends Composite implements IPaneComposite { @IWorkspaceContextService protected contextService: IWorkspaceContextService ) { super(id, telemetryService, themeService, storageService); - - this.menuActions = this._register(this.instantiationService.createInstance(ViewContainerMenuActions, this.getId(), MenuId.ViewContainerTitleContext)); this._register(this.viewPaneContainer.onTitleAreaUpdate(() => this.updateTitleArea())); } @@ -71,22 +65,36 @@ export class PaneComposite extends Composite implements IPaneComposite { getContextMenuActions(): ReadonlyArray { const result = []; - result.push(...this.menuActions.getContextMenuActions()); + result.push(...this.viewPaneContainer.getContextMenuActions2()); - if (result.length) { + const otherActions = this.viewPaneContainer.getContextMenuActions(); + + if (otherActions.length) { result.push(new Separator()); + result.push(...otherActions); } - result.push(...this.viewPaneContainer.getContextMenuActions()); return result; } getActions(): ReadonlyArray { - return this.viewPaneContainer.getActions(); + const result = []; + result.push(...this.viewPaneContainer.getActions2()); + result.push(...this.viewPaneContainer.getActions()); + return result; } getSecondaryActions(): ReadonlyArray { - return this.viewPaneContainer.getSecondaryActions(); + const menuActions = this.viewPaneContainer.getSecondaryActions2(); + const viewPaneContainerActions = this.viewPaneContainer.getSecondaryActions(); + if (menuActions.length && viewPaneContainerActions.length) { + return [ + ...menuActions, + new Separator(), + ...viewPaneContainerActions + ]; + } + return menuActions.length ? menuActions : viewPaneContainerActions; } getActionViewItem(action: IAction): IActionViewItem | undefined { diff --git a/src/vs/workbench/browser/panel.ts b/src/vs/workbench/browser/panel.ts index b2b2a18b..0d04a6a1 100644 --- a/src/vs/workbench/browser/panel.ts +++ b/src/vs/workbench/browser/panel.ts @@ -6,23 +6,74 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IPanel } from 'vs/workbench/common/panel'; import { CompositeDescriptor, CompositeRegistry } from 'vs/workbench/browser/composite'; -import { IConstructorSignature0, BrandedService } from 'vs/platform/instantiation/common/instantiation'; +import { IConstructorSignature0, BrandedService, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { assertIsDefined } from 'vs/base/common/types'; import { PaneComposite } from 'vs/workbench/browser/panecomposite'; +import { IAction, Separator } from 'vs/base/common/actions'; +import { CompositeMenuActions } from 'vs/workbench/browser/menuActions'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +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 { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -export abstract class Panel extends PaneComposite implements IPanel { } +export abstract class Panel extends PaneComposite implements IPanel { + + private readonly panelActions: CompositeMenuActions; + + constructor(id: string, + viewPaneContainer: ViewPaneContainer, + @ITelemetryService telemetryService: ITelemetryService, + @IStorageService storageService: IStorageService, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IContextMenuService contextMenuService: IContextMenuService, + @IExtensionService extensionService: IExtensionService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + ) { + super(id, viewPaneContainer, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); + this.panelActions = this._register(this.instantiationService.createInstance(CompositeMenuActions, MenuId.PanelTitle, undefined, undefined)); + this._register(this.panelActions.onDidChange(() => this.updateTitleArea())); + } + + getActions(): ReadonlyArray { + return [...super.getActions(), ...this.panelActions.getPrimaryActions()]; + } + + getSecondaryActions(): ReadonlyArray { + return this.mergeSecondaryActions(super.getSecondaryActions(), this.panelActions.getSecondaryActions()); + } + + getContextMenuActions(): ReadonlyArray { + return this.mergeSecondaryActions(super.getContextMenuActions(), this.panelActions.getContextMenuActions()); + } + + private mergeSecondaryActions(actions: ReadonlyArray, panelActions: IAction[]): ReadonlyArray { + if (panelActions.length && actions.length) { + return [ + ...actions, + new Separator(), + ...panelActions, + ]; + } + return panelActions.length ? panelActions : actions; + } +} /** * A panel descriptor is a leightweight descriptor of a panel in the workbench. */ export class PanelDescriptor extends CompositeDescriptor { - static create(ctor: { new(...services: Services): Panel }, id: string, name: string, cssClass?: string, order?: number, requestedIndex?: number, _commandId?: string): PanelDescriptor { - return new PanelDescriptor(ctor as IConstructorSignature0, id, name, cssClass, order, requestedIndex, _commandId); + static create(ctor: { new(...services: Services): Panel }, id: string, name: string, cssClass?: string, order?: number, requestedIndex?: number): PanelDescriptor { + return new PanelDescriptor(ctor as IConstructorSignature0, id, name, cssClass, order, requestedIndex); } - private constructor(ctor: IConstructorSignature0, id: string, name: string, cssClass?: string, order?: number, requestedIndex?: number, _commandId?: string) { - super(ctor, id, name, cssClass, order, requestedIndex, _commandId); + private constructor(ctor: IConstructorSignature0, id: string, name: string, cssClass?: string, order?: number, requestedIndex?: number) { + super(ctor, id, name, cssClass, order, requestedIndex); } } diff --git a/src/vs/workbench/browser/part.ts b/src/vs/workbench/browser/part.ts index dfab3bc5..8e78cbcc 100644 --- a/src/vs/workbench/browser/part.ts +++ b/src/vs/workbench/browser/part.ts @@ -162,7 +162,7 @@ class PartLayout { if (this.options && this.options.hasTitle) { titleSize = new Dimension(width, Math.min(height, PartLayout.TITLE_HEIGHT)); } else { - titleSize = new Dimension(0, 0); + titleSize = Dimension.None; } let contentWidth = width; diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index 49bbae45..485e4851 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -4,18 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/activityaction'; -import * as nls from 'vs/nls'; -import * as DOM from 'vs/base/browser/dom'; +import { localize } from 'vs/nls'; +import { EventType, addDisposableListener, EventHelper } from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; -import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; +import { Action, IAction, Separator, SubmenuAction, toAction } from 'vs/base/common/actions'; import { KeyCode } from 'vs/base/common/keyCodes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IMenuService, MenuId, IMenu, registerAction2, Action2, IAction2Options } from 'vs/platform/actions/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { activeContrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry'; -import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ActivityAction, ActivityActionViewItem, ICompositeBar, ICompositeBarColors, ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositeBarActions'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { IActivity } from 'vs/workbench/common/activity'; @@ -29,12 +29,12 @@ import { isMacintosh, isWeb } from 'vs/base/common/platform'; import { getCurrentAuthenticationSessionInfo, IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; import { AuthenticationSession } from 'vs/editor/common/modes'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IProductService } from 'vs/platform/product/common/productService'; import { AnchorAlignment, AnchorAxisAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { getTitleBarStyle } from 'vs/platform/windows/common/windows'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; export class ViewContainerActivityAction extends ActivityAction { @@ -97,10 +97,10 @@ export class ViewContainerActivityAction extends ActivityAction { private logAction(action: string) { type ActivityBarActionClassification = { - viewletId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - action: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + viewletId: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; + action: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; }; - this.telemetryService.publicLog2<{ viewletId: String, action: String }, ActivityBarActionClassification>('activityBarAction', { viewletId: this.activity.id, action }); + this.telemetryService.publicLog2<{ viewletId: String, action: String; }, ActivityBarActionClassification>('activityBarAction', { viewletId: this.activity.id, action }); } } @@ -109,6 +109,7 @@ class MenuActivityActionViewItem extends ActivityActionViewItem { constructor( private readonly menuId: MenuId, action: ActivityAction, + private contextMenuActionsProvider: () => IAction[], colors: (theme: IColorTheme) => ICompositeBarColors, @IThemeService themeService: IThemeService, @IMenuService protected readonly menuService: IMenuService, @@ -126,30 +127,35 @@ class MenuActivityActionViewItem extends ActivityActionViewItem { // Context menus are triggered on mouse down so that an item can be picked // and executed with releasing the mouse over it - this._register(DOM.addDisposableListener(this.container, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => { - DOM.EventHelper.stop(e, true); + this._register(addDisposableListener(this.container, EventType.MOUSE_DOWN, (e: MouseEvent) => { + EventHelper.stop(e, true); this.showContextMenu(e); })); - this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_UP, (e: KeyboardEvent) => { + this._register(addDisposableListener(this.container, EventType.KEY_UP, (e: KeyboardEvent) => { let event = new StandardKeyboardEvent(e); if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { - DOM.EventHelper.stop(e, true); + EventHelper.stop(e, true); this.showContextMenu(); } })); - this._register(DOM.addDisposableListener(this.container, TouchEventType.Tap, (e: GestureEvent) => { - DOM.EventHelper.stop(e, true); + this._register(addDisposableListener(this.container, TouchEventType.Tap, (e: GestureEvent) => { + EventHelper.stop(e, true); this.showContextMenu(); })); } - protected async showContextMenu(e?: MouseEvent): Promise { + private async showContextMenu(e?: MouseEvent): Promise { const disposables = new DisposableStore(); - const menu = disposables.add(this.menuService.createMenu(this.menuId, this.contextKeyService)); - const actions = await this.resolveActions(menu, disposables); + let actions: IAction[]; + if (e?.button !== 2) { + const menu = disposables.add(this.menuService.createMenu(this.menuId, this.contextKeyService)); + actions = await this.resolveMainMenuActions(menu, disposables); + } else { + actions = await this.resolveContextMenuActions(disposables); + } const isUsingCustomMenu = isWeb || (getTitleBarStyle(this.configurationService) !== 'native' && !isMacintosh); // see #40262 const position = this.configurationService.getValue('workbench.sideBar.location'); @@ -163,13 +169,17 @@ class MenuActivityActionViewItem extends ActivityActionViewItem { }); } - protected async resolveActions(menu: IMenu, disposables: DisposableStore): Promise { + protected async resolveMainMenuActions(menu: IMenu, disposables: DisposableStore): Promise { const actions: IAction[] = []; disposables.add(createAndFillInActionBarActions(menu, undefined, { primary: [], secondary: actions })); return actions; } + + protected async resolveContextMenuActions(disposables: DisposableStore): Promise { + return this.contextMenuActionsProvider(); + } } export class HomeActivityActionViewItem extends MenuActivityActionViewItem { @@ -179,6 +189,7 @@ export class HomeActivityActionViewItem extends MenuActivityActionViewItem { constructor( private readonly goHomeHref: string, action: ActivityAction, + contextMenuActionsProvider: () => IAction[], colors: (theme: IColorTheme) => ICompositeBarColors, @IThemeService themeService: IThemeService, @IMenuService menuService: IMenuService, @@ -188,27 +199,32 @@ export class HomeActivityActionViewItem extends MenuActivityActionViewItem { @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IStorageService private readonly storageService: IStorageService ) { - super(MenuId.MenubarHomeMenu, action, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService); + super(MenuId.MenubarHomeMenu, action, contextMenuActionsProvider, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService); } - protected async resolveActions(homeMenu: IMenu, disposables: DisposableStore): Promise { + protected async resolveMainMenuActions(homeMenu: IMenu, disposables: DisposableStore): Promise { const actions = []; // Go Home - actions.push(disposables.add(new Action('goHome', nls.localize('goHome', "Go Home"), undefined, true, async () => window.location.href = this.goHomeHref))); - actions.push(disposables.add(new Separator())); + actions.push(toAction({ id: 'goHome', label: localize('goHome', "Go Home"), run: () => window.location.href = this.goHomeHref })); // Contributed - const contributedActions = await super.resolveActions(homeMenu, disposables); - actions.push(...contributedActions); - - // Hide - if (contributedActions.length > 0) { + const contributedActions = await super.resolveMainMenuActions(homeMenu, disposables); + if (contributedActions.length) { actions.push(disposables.add(new Separator())); + actions.push(...contributedActions); } - actions.push(disposables.add(new Action('hide', nls.localize('hide', "Hide"), undefined, true, async () => { - this.storageService.store(HomeActivityActionViewItem.HOME_BAR_VISIBILITY_PREFERENCE, false, StorageScope.GLOBAL, StorageTarget.USER); - }))); + + return actions; + } + + protected async resolveContextMenuActions(disposables: DisposableStore): Promise { + const actions = await super.resolveContextMenuActions(disposables); + + actions.unshift(...[ + toAction({ id: 'hideHomeButton', label: localize('hideHomeButton', "Hide Home Button"), run: () => this.storageService.store(HomeActivityActionViewItem.HOME_BAR_VISIBILITY_PREFERENCE, false, StorageScope.GLOBAL, StorageTarget.USER) }), + new Separator() + ]); return actions; } @@ -220,6 +236,7 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { constructor( action: ActivityAction, + contextMenuActionsProvider: () => IAction[], colors: (theme: IColorTheme) => ICompositeBarColors, @IThemeService themeService: IThemeService, @IContextMenuService contextMenuService: IContextMenuService, @@ -227,15 +244,15 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { @IContextKeyService contextKeyService: IContextKeyService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @IStorageService private readonly storageService: IStorageService, @IProductService private readonly productService: IProductService, @IConfigurationService configurationService: IConfigurationService, + @IStorageService private readonly storageService: IStorageService ) { - super(MenuId.AccountsContext, action, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService); + super(MenuId.AccountsContext, action, contextMenuActionsProvider, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService); } - protected async resolveActions(accountsMenu: IMenu, disposables: DisposableStore): Promise { - await super.resolveActions(accountsMenu, disposables); + protected async resolveMainMenuActions(accountsMenu: IMenu, disposables: DisposableStore): Promise { + await super.resolveMainMenuActions(accountsMenu, disposables); const otherCommands = accountsMenu.getActions(); const providers = this.authenticationService.getProviderIds(); @@ -243,7 +260,7 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { try { const sessions = await this.authenticationService.getSessions(providerId); - const groupedSessions: { [label: string]: AuthenticationSession[] } = {}; + const groupedSessions: { [label: string]: AuthenticationSession[]; } = {}; sessions.forEach(session => { if (groupedSessions[session.account.label]) { groupedSessions[session.account.label].push(session); @@ -266,11 +283,11 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { if (sessionInfo.sessions) { Object.keys(sessionInfo.sessions).forEach(accountName => { - const manageExtensionsAction = disposables.add(new Action(`configureSessions${accountName}`, nls.localize('manageTrustedExtensions', "Manage Trusted Extensions"), '', true, () => { + const manageExtensionsAction = disposables.add(new Action(`configureSessions${accountName}`, localize('manageTrustedExtensions', "Manage Trusted Extensions"), '', true, () => { return this.authenticationService.manageTrustedExtensionsForAccount(sessionInfo.providerId, accountName); })); - const signOutAction = disposables.add(new Action('signOut', nls.localize('signOut', "Sign Out"), '', true, () => { + const signOutAction = disposables.add(new Action('signOut', localize('signOut', "Sign Out"), '', true, () => { return this.authenticationService.signOutOfAccount(sessionInfo.providerId, accountName); })); @@ -285,7 +302,7 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { menus.push(providerSubMenu); }); } else { - const providerUnavailableAction = disposables.add(new Action('providerUnavailable', nls.localize('authProviderUnavailable', '{0} is currently unavailable', providerDisplayName))); + const providerUnavailableAction = disposables.add(new Action('providerUnavailable', localize('authProviderUnavailable', '{0} is currently unavailable', providerDisplayName))); menus.push(providerUnavailableAction); } }); @@ -302,22 +319,26 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { } }); - if (menus.length) { - menus.push(disposables.add(new Separator())); - } - - menus.push(disposables.add(new Action('hide', nls.localize('hide', "Hide"), undefined, true, async () => { - this.storageService.store(AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, false, StorageScope.GLOBAL, StorageTarget.USER); - }))); - return menus; } + + protected async resolveContextMenuActions(disposables: DisposableStore): Promise { + const actions = await super.resolveContextMenuActions(disposables); + + actions.unshift(...[ + toAction({ id: 'hideAccounts', label: localize('hideAccounts', "Hide Accounts"), run: () => this.storageService.store(AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, false, StorageScope.GLOBAL, StorageTarget.USER) }), + new Separator() + ]); + + return actions; + } } export class GlobalActivityActionViewItem extends MenuActivityActionViewItem { constructor( action: ActivityAction, + contextMenuActionsProvider: () => IAction[], colors: (theme: IColorTheme) => ICompositeBarColors, @IThemeService themeService: IThemeService, @IMenuService menuService: IMenuService, @@ -326,7 +347,7 @@ export class GlobalActivityActionViewItem extends MenuActivityActionViewItem { @IConfigurationService configurationService: IConfigurationService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { - super(MenuId.GlobalActivity, action, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService); + super(MenuId.GlobalActivity, action, contextMenuActionsProvider, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService); } } @@ -379,7 +400,7 @@ registerAction2( constructor() { super({ id: 'workbench.action.previousSideBarView', - title: { value: nls.localize('previousSideBarView', "Previous Side Bar View"), original: 'Previous Side Bar View' }, + title: { value: localize('previousSideBarView', "Previous Side Bar View"), original: 'Previous Side Bar View' }, category: CATEGORIES.View, f1: true }, -1); @@ -392,7 +413,7 @@ registerAction2( constructor() { super({ id: 'workbench.action.nextSideBarView', - title: { value: nls.localize('nextSideBarView', "Next Side Bar View"), original: 'Next Side Bar View' }, + title: { value: localize('nextSideBarView', "Next Side Bar View"), original: 'Next Side Bar View' }, category: CATEGORIES.View, f1: true }, 1); @@ -400,7 +421,7 @@ registerAction2( } ); -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { const activityBarBackgroundColor = theme.getColor(ACTIVITY_BAR_BACKGROUND); if (activityBarBackgroundColor) { collector.addRule(` diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 39a3ca76..4e78ba22 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/activitybarpart'; -import * as nls from 'vs/nls'; +import { localize } from 'vs/nls'; import { ActionsOrientation, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { GLOBAL_ACTIVITY_ID, IActivity, ACCOUNTS_ACTIVITY_ID } from 'vs/workbench/common/activity'; import { Part } from 'vs/workbench/browser/part'; @@ -13,7 +13,7 @@ import { IBadge, NumberBadge } from 'vs/workbench/services/activity/common/activ import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; -import { ToggleActivityBarVisibilityAction, ToggleMenuBarAction, ToggleSidebarPositionAction } from 'vs/workbench/browser/actions/layoutActions'; +import { ToggleActivityBarVisibilityAction, ToggleSidebarPositionAction } from 'vs/workbench/browser/actions/layoutActions'; import { IThemeService, IColorTheme, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ACTIVITY_BAR_BACKGROUND, ACTIVITY_BAR_BORDER, ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_ACTIVE_BORDER, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_INACTIVE_FOREGROUND, ACTIVITY_BAR_ACTIVE_BACKGROUND, ACTIVITY_BAR_DRAG_AND_DROP_BORDER } from 'vs/workbench/common/theme'; import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; @@ -23,33 +23,34 @@ import { IStorageService, StorageScope, IStorageValueChangeEvent, StorageTarget import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ToggleCompositePinnedAction, ICompositeBarColors, ActivityAction, ICompositeActivity } from 'vs/workbench/browser/parts/compositeBarActions'; -import { IViewDescriptorService, ViewContainer, TEST_VIEW_CONTAINER_ID, IViewContainerModel, ViewContainerLocation, IViewsService } from 'vs/workbench/common/views'; +import { IViewDescriptorService, ViewContainer, IViewContainerModel, ViewContainerLocation, IViewsService } from 'vs/workbench/common/views'; import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { assertIsDefined } from 'vs/base/common/types'; +import { assertIsDefined, isString } from 'vs/base/common/types'; import { IActivityBarService } from 'vs/workbench/services/activityBar/browser/activityBarService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { Schemas } from 'vs/base/common/network'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { CustomMenubarControl } from 'vs/workbench/browser/parts/titlebar/menubarControl'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { getMenuBarVisibility } from 'vs/platform/windows/common/windows'; -import { isWeb } from 'vs/base/common/platform'; +import { isNative, isWeb } from 'vs/base/common/platform'; import { Before2D } from 'vs/workbench/browser/dnd'; import { Codicon, iconRegistry } from 'vs/base/common/codicons'; -import { Action, Separator } from 'vs/base/common/actions'; +import { IAction, Separator, toAction } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { StringSHA1 } from 'vs/base/common/hash'; interface IPlaceholderViewContainer { readonly id: string; readonly name?: string; readonly iconUrl?: UriComponents; readonly themeIcon?: ThemeIcon; - readonly views?: { when?: string }[]; + readonly isBuiltin?: boolean; + readonly views?: { when?: string; }[]; } interface IPinnedViewContainer { @@ -66,12 +67,10 @@ interface ICachedViewContainer { readonly pinned: boolean; readonly order?: number; visible: boolean; - views?: { when?: string }[]; + isBuiltin?: boolean; + views?: { when?: string; }[]; } -const settingsViewBarIcon = registerIcon('settings-view-bar-icon', Codicon.settingsGear, nls.localize('settingsViewBarIcon', 'Settings icon in the view bar.')); -const accountsViewBarIcon = registerIcon('accounts-view-bar-icon', Codicon.account, nls.localize('accountsViewBarIcon', 'Accounts icon in the view bar.')); - export class ActivitybarPart extends Part implements IActivityBarService { declare readonly _serviceBrand: undefined; @@ -81,6 +80,9 @@ export class ActivitybarPart extends Part implements IActivityBarService { private static readonly ACTION_HEIGHT = 48; private static readonly ACCOUNTS_ACTION_INDEX = 0; + private static readonly GEAR_ICON = registerIcon('settings-view-bar-icon', Codicon.settingsGear, localize('settingsViewBarIcon', "Settings icon in the view bar.")); + private static readonly ACCOUNTS_ICON = registerIcon('accounts-view-bar-icon', Codicon.account, localize('accountsViewBarIcon', "Accounts icon in the view bar.")); + //#region IView readonly minimumWidth: number = 48; @@ -110,12 +112,13 @@ export class ActivitybarPart extends Part implements IActivityBarService { private readonly accountsActivity: ICompositeActivity[] = []; - private readonly compositeActions = new Map(); + private readonly compositeActions = new Map(); private readonly viewContainerDisposables = new Map(); private readonly keyboardNavigationDisposables = this._register(new DisposableStore()); private readonly location = ViewContainerLocation.Sidebar; + private hasExtensionsRegistered: boolean = false; constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -132,14 +135,8 @@ export class ActivitybarPart extends Part implements IActivityBarService { super(Parts.ACTIVITYBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); for (const cachedViewContainer of this.cachedViewContainers) { - if ( - environmentService.remoteAuthority || // In remote window, hide activity bar entries until registered - this.shouldBeHidden(cachedViewContainer.id, cachedViewContainer) - ) { - cachedViewContainer.visible = false; - } + cachedViewContainer.visible = !this.shouldBeHidden(cachedViewContainer.id, cachedViewContainer); } - this.compositeBar = this.createCompositeBar(); this.onDidRegisterViewContainers(this.getViewContainers()); @@ -161,53 +158,66 @@ export class ActivitybarPart extends Part implements IActivityBarService { icon: true, orientation: ActionsOrientation.VERTICAL, preventLoopNavigation: true, - openComposite: (compositeId: string) => this.viewsService.openViewContainer(compositeId, true), - getActivityAction: (compositeId: string) => this.getCompositeActions(compositeId).activityAction, - getCompositePinnedAction: (compositeId: string) => this.getCompositeActions(compositeId).pinnedAction, - getOnCompositeClickAction: (compositeId: string) => new Action(compositeId, '', '', true, () => this.viewsService.isViewContainerVisible(compositeId) ? Promise.resolve(this.viewsService.closeViewContainer(compositeId)) : this.viewsService.openViewContainer(compositeId)), - getContextMenuActions: () => { - const actions = []; + openComposite: compositeId => this.viewsService.openViewContainer(compositeId, true), + getActivityAction: compositeId => this.getCompositeActions(compositeId).activityAction, + getCompositePinnedAction: compositeId => this.getCompositeActions(compositeId).pinnedAction, + getOnCompositeClickAction: compositeId => toAction({ id: compositeId, label: '', run: async () => this.viewsService.isViewContainerVisible(compositeId) ? this.viewsService.closeViewContainer(compositeId) : this.viewsService.openViewContainer(compositeId) }), + fillExtraContextMenuActions: actions => { // Home + const topActions: IAction[] = []; if (this.homeBarContainer) { - actions.push(new Action( - 'toggleHomeBarAction', - this.homeBarVisibilityPreference ? nls.localize('hideHomeBar', "Hide Home Button") : nls.localize('showHomeBar', "Show Home Button"), - undefined, - true, - async () => { this.homeBarVisibilityPreference = !this.homeBarVisibilityPreference; } - )); + topActions.push({ + id: 'toggleHomeBarAction', + label: localize('homeButton', "Home Button"), + class: undefined, + tooltip: localize('homeButton', "Home Button"), + checked: this.homeBarVisibilityPreference, + enabled: true, + run: async () => this.homeBarVisibilityPreference = !this.homeBarVisibilityPreference, + dispose: () => { } + }); } // Menu const menuBarVisibility = getMenuBarVisibility(this.configurationService); if (menuBarVisibility === 'compact' || (menuBarVisibility === 'hidden' && isWeb)) { - actions.push(this.instantiationService.createInstance(ToggleMenuBarAction, ToggleMenuBarAction.ID, menuBarVisibility === 'compact' ? nls.localize('hideMenu', "Hide Menu") : nls.localize('showMenu', "Show Menu"))); + topActions.push({ + id: 'toggleMenuVisibility', + label: localize('menu', "Menu"), + class: undefined, + tooltip: localize('menu', "Menu"), + checked: menuBarVisibility === 'compact', + enabled: true, + run: async () => this.layoutService.toggleMenuBar(), + dispose: () => { } + }); + } + + if (topActions.length) { + actions.unshift(...topActions, new Separator()); } // Accounts - actions.push(new Action( - 'toggleAccountsVisibility', - this.accountsVisibilityPreference ? nls.localize('hideAccounts', "Hide Accounts") : nls.localize('showAccounts', "Show Accounts"), - undefined, - true, - async () => { this.accountsVisibilityPreference = !this.accountsVisibilityPreference; } - )); + actions.push(new Separator()); + actions.push({ + id: 'toggleAccountsVisibility', + label: localize('accounts', "Accounts"), + class: undefined, + tooltip: localize('accounts', "Accounts"), + checked: this.accountsVisibilityPreference, + enabled: true, + run: async () => this.accountsVisibilityPreference = !this.accountsVisibilityPreference, + dispose: () => { } + }); + actions.push(new Separator()); // Toggle Sidebar actions.push(this.instantiationService.createInstance(ToggleSidebarPositionAction, ToggleSidebarPositionAction.ID, ToggleSidebarPositionAction.getLabel(this.layoutService))); // Toggle Activity Bar - actions.push(new Action( - ToggleActivityBarVisibilityAction.ID, - nls.localize('hideActivitBar', "Hide Activity Bar"), - undefined, - true, - async () => { this.instantiationService.invokeFunction(accessor => new ToggleActivityBarVisibilityAction().run(accessor)); } - )); - - return actions; + actions.push(toAction({ id: ToggleActivityBarVisibilityAction.ID, label: localize('hideActivitBar', "Hide Activity Bar"), run: async () => this.instantiationService.invokeFunction(accessor => new ToggleActivityBarVisibilityAction().run(accessor)) })); }, getContextMenuActionsForComposite: compositeId => this.getContextMenuActionsForComposite(compositeId), getDefaultCompositeId: () => this.viewDescriptorService.getDefaultViewContainer(this.location)!.id, @@ -223,24 +233,20 @@ export class ActivitybarPart extends Part implements IActivityBarService { })); } - private getContextMenuActionsForComposite(compositeId: string): Action[] { - const actions = []; + private getContextMenuActionsForComposite(compositeId: string): IAction[] { + const actions: IAction[] = []; const viewContainer = this.viewDescriptorService.getViewContainerById(compositeId)!; const defaultLocation = this.viewDescriptorService.getDefaultViewContainerLocation(viewContainer)!; if (defaultLocation !== this.viewDescriptorService.getViewContainerLocation(viewContainer)) { - actions.push(new Action('resetLocationAction', nls.localize('resetLocation', "Reset Location"), undefined, true, async () => { - this.viewDescriptorService.moveViewContainerToLocation(viewContainer, defaultLocation); - })); + actions.push(toAction({ id: 'resetLocationAction', label: localize('resetLocation', "Reset Location"), run: () => this.viewDescriptorService.moveViewContainerToLocation(viewContainer, defaultLocation) })); } else { const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); if (viewContainerModel.allViewDescriptors.length === 1) { const viewToReset = viewContainerModel.allViewDescriptors[0]; const defaultContainer = this.viewDescriptorService.getDefaultContainerById(viewToReset.id)!; if (defaultContainer !== viewContainer) { - actions.push(new Action('resetLocationAction', nls.localize('resetLocation', "Reset Location"), undefined, true, async () => { - this.viewDescriptorService.moveViewsToContainer([viewToReset], defaultContainer); - })); + actions.push(toAction({ id: 'resetLocationAction', label: localize('resetLocation', "Reset Location"), run: () => this.viewDescriptorService.moveViewsToContainer([viewToReset], defaultContainer) })); } } } @@ -278,7 +284,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { })); } - private onDidChangeViewContainers(added: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation }>, removed: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation }>) { + private onDidChangeViewContainers(added: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation; }>, removed: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation; }>) { removed.filter(({ location }) => location === ViewContainerLocation.Sidebar).forEach(({ container }) => this.onDidDeregisterViewContainer(container)); this.onDidRegisterViewContainers(added.filter(({ location }) => location === ViewContainerLocation.Sidebar).map(({ container }) => container)); } @@ -310,7 +316,22 @@ export class ActivitybarPart extends Part implements IActivityBarService { } private onDidRegisterExtensions(): void { - this.removeNotExistingComposites(); + this.hasExtensionsRegistered = true; + + // show/hide/remove composites + for (const { id } of this.cachedViewContainers) { + const viewContainer = this.getViewContainer(id); + if (viewContainer) { + this.showOrHideViewContainer(viewContainer); + } else { + if (this.viewDescriptorService.isViewContainerRemovedPermanently(id)) { + this.removeComposite(id); + } else { + this.hideComposite(id); + } + } + } + this.saveCachedViewContainers(); } @@ -322,7 +343,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.compositeBar.addComposite(viewContainer); this.compositeBar.activateComposite(viewContainer.id); - if (viewContainer.hideIfEmpty) { + if (this.shouldBeHidden(viewContainer)) { const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); if (viewContainerModel.activeViewDescriptors.length === 0) { // Update the composite bar by hiding @@ -460,7 +481,8 @@ export class ActivitybarPart extends Part implements IActivityBarService { // Home action bar const homeIndicator = this.environmentService.options?.homeIndicator; - if (homeIndicator) { + // TODO @sbatten remove the fake setting and associated code + if (homeIndicator && this.configurationService.getValue('window.showHomeIndicator')) { let codicon = iconRegistry.get(homeIndicator.icon); if (!codicon) { codicon = Codicon.code; @@ -556,14 +578,14 @@ export class ActivitybarPart extends Part implements IActivityBarService { private createHomeBar(href: string, icon: Codicon): void { this.homeBarContainer = document.createElement('div'); - this.homeBarContainer.setAttribute('aria-label', nls.localize('homeIndicator', "Home")); + this.homeBarContainer.setAttribute('aria-label', localize('homeIndicator', "Home")); this.homeBarContainer.setAttribute('role', 'toolbar'); this.homeBarContainer.classList.add('home-bar'); this.homeBar = this._register(new ActionBar(this.homeBarContainer, { - actionViewItemProvider: action => this.instantiationService.createInstance(HomeActivityActionViewItem, href, action as ActivityAction, (theme: IColorTheme) => this.getActivitybarItemColors(theme)), + actionViewItemProvider: action => this.instantiationService.createInstance(HomeActivityActionViewItem, href, action as ActivityAction, () => this.compositeBar.getContextMenuActions(), (theme: IColorTheme) => this.getActivitybarItemColors(theme)), orientation: ActionsOrientation.VERTICAL, - ariaLabel: nls.localize('home', "Home"), + ariaLabel: localize('home', "Home"), animated: false, preventLoopNavigation: true, ignoreOrientationForPreviousAndNextKey: true @@ -575,7 +597,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.homeBar.push(this._register(new ActivityAction({ id: 'workbench.actions.home', - name: nls.localize('home', "Home"), + name: localize('home', "Home"), cssClass: icon.classNames }))); @@ -587,17 +609,17 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.globalActivityActionBar = this._register(new ActionBar(container, { actionViewItemProvider: action => { if (action.id === 'workbench.actions.manage') { - return this.instantiationService.createInstance(GlobalActivityActionViewItem, action as ActivityAction, (theme: IColorTheme) => this.getActivitybarItemColors(theme)); + return this.instantiationService.createInstance(GlobalActivityActionViewItem, action as ActivityAction, () => this.compositeBar.getContextMenuActions(), (theme: IColorTheme) => this.getActivitybarItemColors(theme)); } if (action.id === 'workbench.actions.accounts') { - return this.instantiationService.createInstance(AccountsActivityActionViewItem, action as ActivityAction, (theme: IColorTheme) => this.getActivitybarItemColors(theme)); + return this.instantiationService.createInstance(AccountsActivityActionViewItem, action as ActivityAction, () => this.compositeBar.getContextMenuActions(), (theme: IColorTheme) => this.getActivitybarItemColors(theme)); } throw new Error(`No view item for action '${action.id}'`); }, orientation: ActionsOrientation.VERTICAL, - ariaLabel: nls.localize('manage', "Manage"), + ariaLabel: localize('manage', "Manage"), animated: false, preventLoopNavigation: true, ignoreOrientationForPreviousAndNextKey: true @@ -605,15 +627,15 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.globalActivityAction = this._register(new ActivityAction({ id: 'workbench.actions.manage', - name: nls.localize('manage', "Manage"), - cssClass: ThemeIcon.asClassName(settingsViewBarIcon) + name: localize('manage', "Manage"), + cssClass: ThemeIcon.asClassName(ActivitybarPart.GEAR_ICON) })); if (this.accountsVisibilityPreference) { this.accountsActivityAction = this._register(new ActivityAction({ id: 'workbench.actions.accounts', - name: nls.localize('accounts', "Accounts"), - cssClass: ThemeIcon.asClassName(accountsViewBarIcon) + name: localize('accounts', "Accounts"), + cssClass: ThemeIcon.asClassName(ActivitybarPart.ACCOUNTS_ICON) })); this.globalActivityActionBar.push(this.accountsActivityAction, { index: ActivitybarPart.ACCOUNTS_ACTION_INDEX }); @@ -630,7 +652,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { } else { this.accountsActivityAction = this._register(new ActivityAction({ id: 'workbench.actions.accounts', - name: nls.localize('accounts', "Accounts"), + name: localize('accounts', "Accounts"), cssClass: Codicon.account.classNames })); this.globalActivityActionBar.push(this.accountsActivityAction, { index: ActivitybarPart.ACCOUNTS_ACTION_INDEX }); @@ -640,7 +662,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.updateGlobalActivity(ACCOUNTS_ACTIVITY_ID); } - private getCompositeActions(compositeId: string): { activityAction: ViewContainerActivityAction, pinnedAction: ToggleCompositePinnedAction } { + private getCompositeActions(compositeId: string): { activityAction: ViewContainerActivityAction, pinnedAction: ToggleCompositePinnedAction; } { let compositeActions = this.compositeActions.get(compositeId); if (!compositeActions) { const viewContainer = this.getViewContainer(compositeId); @@ -666,32 +688,27 @@ export class ActivitybarPart extends Part implements IActivityBarService { private onDidRegisterViewContainers(viewContainers: ReadonlyArray): void { for (const viewContainer of viewContainers) { + this.compositeBar.addComposite(viewContainer); + + // Pin it by default if it is new const cachedViewContainer = this.cachedViewContainers.filter(({ id }) => id === viewContainer.id)[0]; - const visibleViewContainer = this.viewsService.getVisibleViewContainer(this.location); - const isActive = visibleViewContainer?.id === viewContainer.id; - - if (isActive || !this.shouldBeHidden(viewContainer.id, cachedViewContainer)) { - this.compositeBar.addComposite(viewContainer); - - // Pin it by default if it is new - if (!cachedViewContainer) { - this.compositeBar.pin(viewContainer.id); - } - - if (isActive) { - this.compositeBar.activateComposite(viewContainer.id); - } + if (!cachedViewContainer) { + this.compositeBar.pin(viewContainer.id); + } + + // Active + const visibleViewContainer = this.viewsService.getVisibleViewContainer(this.location); + if (visibleViewContainer?.id === viewContainer.id) { + this.compositeBar.activateComposite(viewContainer.id); } - } - for (const viewContainer of viewContainers) { const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); this.updateActivity(viewContainer, viewContainerModel); - this.onDidChangeActiveViews(viewContainer, viewContainerModel); + this.showOrHideViewContainer(viewContainer); const disposables = new DisposableStore(); disposables.add(viewContainerModel.onDidChangeContainerInfo(() => this.updateActivity(viewContainer, viewContainerModel))); - disposables.add(viewContainerModel.onDidChangeActiveViewDescriptors(() => this.onDidChangeActiveViews(viewContainer, viewContainerModel))); + disposables.add(viewContainerModel.onDidChangeActiveViewDescriptors(() => this.showOrHideViewContainer(viewContainer))); this.viewContainerDisposables.set(viewContainer.id, disposables); } @@ -728,12 +745,15 @@ export class ActivitybarPart extends Part implements IActivityBarService { let iconUrl: URI | undefined = undefined; if (URI.isUri(icon)) { iconUrl = icon; - cssClass = `activity-${id.replace(/\./g, '-')}`; + const cssUrl = asCSSUrl(icon); + const hash = new StringSHA1(); + hash.update(cssUrl); + cssClass = `activity-${id.replace(/\./g, '-')}-${hash.digest()}`; const iconClass = `.monaco-workbench .activitybar .monaco-action-bar .action-label.${cssClass}`; createCSSRule(iconClass, ` - mask: ${asCSSUrl(icon)} no-repeat 50% 50%; + mask: ${cssUrl} no-repeat 50% 50%; mask-size: 24px; - -webkit-mask: ${asCSSUrl(icon)} no-repeat 50% 50%; + -webkit-mask: ${cssUrl} no-repeat 50% 50%; -webkit-mask-size: 24px; `); } else if (ThemeIcon.isThemeIcon(icon)) { @@ -743,36 +763,43 @@ export class ActivitybarPart extends Part implements IActivityBarService { return { id, name, cssClass, iconUrl, keybindingId }; } - private onDidChangeActiveViews(viewContainer: ViewContainer, viewContainerModel: IViewContainerModel): void { - if (viewContainerModel.activeViewDescriptors.length) { - this.compositeBar.addComposite(viewContainer); - } else if (viewContainer.hideIfEmpty) { + private showOrHideViewContainer(viewContainer: ViewContainer): void { + if (this.shouldBeHidden(viewContainer)) { this.hideComposite(viewContainer.id); + } else { + this.compositeBar.addComposite(viewContainer); } } - private shouldBeHidden(viewContainerId: string, cachedViewContainer?: ICachedViewContainer): boolean { - const viewContainer = this.getViewContainer(viewContainerId); - if (!viewContainer || !viewContainer.hideIfEmpty) { - return false; - } + private shouldBeHidden(viewContainerOrId: string | ViewContainer, cachedViewContainer?: ICachedViewContainer): boolean { + const viewContainer = isString(viewContainerOrId) ? this.getViewContainer(viewContainerOrId) : viewContainerOrId; + const viewContainerId = isString(viewContainerOrId) ? viewContainerOrId : viewContainerOrId.id; - return cachedViewContainer?.views && cachedViewContainer.views.length - ? cachedViewContainer.views.every(({ when }) => !!when && !this.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(when))) - : viewContainerId === TEST_VIEW_CONTAINER_ID /* Hide Test view container for the first time or it had no views registered before */; - } - - private removeNotExistingComposites(): void { - const viewContainers = this.getViewContainers(); - for (const { id } of this.cachedViewContainers) { - if (viewContainers.every(viewContainer => viewContainer.id !== id)) { - if (this.viewDescriptorService.isViewContainerRemovedPermanently(id)) { - this.removeComposite(id); - } else { - this.hideComposite(id); + if (viewContainer) { + if (viewContainer.hideIfEmpty) { + if (this.viewDescriptorService.getViewContainerModel(viewContainer).activeViewDescriptors.length > 0) { + return false; } + } else { + return false; } } + + // Check cache only if extensions are not yet registered and current window is not native (desktop) remote connection window + if (!this.hasExtensionsRegistered && !(this.environmentService.remoteAuthority && isNative)) { + cachedViewContainer = cachedViewContainer || this.cachedViewContainers.find(({ id }) => id === viewContainerId); + + // Show builtin ViewContainer if not registered yet + if (!viewContainer && cachedViewContainer?.isBuiltin) { + return false; + } + + if (cachedViewContainer?.views?.length) { + return cachedViewContainer.views.every(({ when }) => !!when && !this.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(when))); + } + } + + return true; } private hideComposite(compositeId: string): void { @@ -864,7 +891,6 @@ export class ActivitybarPart extends Part implements IActivityBarService { private getViewContainer(id: string): ViewContainer | undefined { const viewContainer = this.viewDescriptorService.getViewContainerById(id); - return viewContainer && this.viewDescriptorService.getViewContainerLocation(viewContainer) === this.location ? viewContainer : undefined; } @@ -918,22 +944,22 @@ export class ActivitybarPart extends Part implements IActivityBarService { const viewContainer = this.getViewContainer(compositeItem.id); if (viewContainer) { const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); - const views: { when: string | undefined }[] = []; + const views: { when: string | undefined; }[] = []; for (const { when } of viewContainerModel.allViewDescriptors) { views.push({ when: when ? when.serialize() : undefined }); } - const cacheIcon = URI.isUri(viewContainerModel.icon) ? viewContainerModel.icon.scheme === Schemas.file : true; state.push({ id: compositeItem.id, name: viewContainerModel.title, - icon: cacheIcon ? viewContainerModel.icon : undefined, + icon: URI.isUri(viewContainerModel.icon) && this.environmentService.remoteAuthority && isNative ? undefined : viewContainerModel.icon, /* Donot cache uri icons in desktop with remote connection */ views, pinned: compositeItem.pinned, order: compositeItem.order, - visible: compositeItem.visible + visible: compositeItem.visible, + isBuiltin: !viewContainer.extensionId }); } else { - state.push({ id: compositeItem.id, pinned: compositeItem.pinned, order: compositeItem.order, visible: false }); + state.push({ id: compositeItem.id, pinned: compositeItem.pinned, order: compositeItem.order, visible: false, isBuiltin: false }); } } @@ -948,9 +974,10 @@ export class ActivitybarPart extends Part implements IActivityBarService { const cachedViewContainer = this._cachedViewContainers.filter(cached => cached.id === placeholderViewContainer.id)[0]; if (cachedViewContainer) { cachedViewContainer.name = placeholderViewContainer.name; - cachedViewContainer.icon = placeholderViewContainer.themeIcon ? ThemeIcon.revive(placeholderViewContainer.themeIcon) : + cachedViewContainer.icon = placeholderViewContainer.themeIcon ? placeholderViewContainer.themeIcon : placeholderViewContainer.iconUrl ? URI.revive(placeholderViewContainer.iconUrl) : undefined; cachedViewContainer.views = placeholderViewContainer.views; + cachedViewContainer.isBuiltin = placeholderViewContainer.isBuiltin; } } } @@ -966,11 +993,12 @@ export class ActivitybarPart extends Part implements IActivityBarService { order }))); - this.setPlaceholderViewContainers(cachedViewContainers.map(({ id, icon, name, views }) => ({ + this.setPlaceholderViewContainers(cachedViewContainers.map(({ id, icon, name, views, isBuiltin }) => ({ id, iconUrl: URI.isUri(icon) ? icon : undefined, themeIcon: ThemeIcon.isThemeIcon(icon) ? icon : undefined, name, + isBuiltin, views }))); } @@ -1067,7 +1095,7 @@ class FocusActivityBarAction extends Action2 { constructor() { super({ id: 'workbench.action.focusActivityBar', - title: { value: nls.localize('focusActivityBar', "Focus Activity Bar"), original: 'Focus Activity Bar' }, + title: { value: localize('focusActivityBar', "Focus Activity Bar"), original: 'Focus Activity Bar' }, category: CATEGORIES.View, f1: true }); diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 06a031a5..70d629fb 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { Action, IAction, Separator } from 'vs/base/common/actions'; +import { IAction, toAction } from 'vs/base/common/actions'; import { illegalArgument } from 'vs/base/common/errors'; import * as arrays from 'vs/base/common/arrays'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -149,10 +149,10 @@ export interface ICompositeBarOptions { readonly preventLoopNavigation?: boolean; getActivityAction: (compositeId: string) => ActivityAction; - getCompositePinnedAction: (compositeId: string) => Action; - getOnCompositeClickAction: (compositeId: string) => Action; - getContextMenuActions: () => Action[]; - getContextMenuActionsForComposite: (compositeId: string) => Action[]; + getCompositePinnedAction: (compositeId: string) => IAction; + getOnCompositeClickAction: (compositeId: string) => IAction; + fillExtraContextMenuActions: (actions: IAction[]) => void; + getContextMenuActionsForComposite: (compositeId: string) => IAction[]; openComposite: (compositeId: string) => Promise; getDefaultCompositeId: () => string; hidePart: () => void; @@ -208,15 +208,15 @@ export class CompositeBar extends Widget implements ICompositeBar { create(parent: HTMLElement): HTMLElement { const actionBarDiv = parent.appendChild($('.composite-bar')); this.compositeSwitcherBar = this._register(new ActionBar(actionBarDiv, { - actionViewItemProvider: (action: IAction) => { + actionViewItemProvider: action => { if (action instanceof CompositeOverflowActivityAction) { return this.compositeOverflowActionViewItem; } const item = this.model.findItem(action.id); return item && this.instantiationService.createInstance( CompositeActionViewItem, action as ActivityAction, item.pinnedAction, - (compositeId: string) => this.options.getContextMenuActionsForComposite(compositeId), - () => this.getContextMenuActions() as Action[], + compositeId => this.options.getContextMenuActionsForComposite(compositeId), + () => this.getContextMenuActions(), this.options.colors, this.options.icon, this.options.dndHandler, @@ -319,7 +319,7 @@ export class CompositeBar extends Widget implements ICompositeBar { this.updateCompositeSwitcher(); } - addComposite({ id, name, order, requestedIndex }: { id: string; name: string, order?: number, requestedIndex?: number }): void { + addComposite({ id, name, order, requestedIndex }: { id: string; name: string, order?: number, requestedIndex?: number; }): void { // Add to the model if (this.model.add(id, name, order, requestedIndex)) { this.computeSizes([this.model.findItem(id)]); @@ -596,7 +596,7 @@ export class CompositeBar extends Widget implements ICompositeBar { this.compositeOverflowAction, () => this.getOverflowingComposites(), () => this.model.activeItem ? this.model.activeItem.id : undefined, - (compositeId: string) => { + compositeId => { const item = this.model.findItem(compositeId); return item?.activity[0]?.badge; }, @@ -610,7 +610,7 @@ export class CompositeBar extends Widget implements ICompositeBar { this._onDidChange.fire(); } - private getOverflowingComposites(): { id: string, name?: string }[] { + private getOverflowingComposites(): { id: string, name?: string; }[] { let overflowingIds = this.model.visibleItems.filter(item => item.pinned).map(item => item.id); // Show the active composite even if it is not pinned @@ -631,9 +631,9 @@ export class CompositeBar extends Widget implements ICompositeBar { }); } - private getContextMenuActions(): IAction[] { + getContextMenuActions(): IAction[] { const actions: IAction[] = this.model.visibleItems - .map(({ id, name, activityAction }) => ({ + .map(({ id, name, activityAction }) => (toAction({ id, label: this.getAction(id).label || name || id, checked: this.isPinned(id), @@ -645,19 +645,17 @@ export class CompositeBar extends Widget implements ICompositeBar { this.pin(id, true); } } - })); - const otherActions = this.options.getContextMenuActions(); - if (otherActions.length) { - actions.push(new Separator()); - actions.push(...otherActions); - } + }))); + + this.options.fillExtraContextMenuActions(actions); + return actions; } } interface ICompositeBarModelItem extends ICompositeBarItem { activityAction: ActivityAction; - pinnedAction: Action; + pinnedAction: IAction; activity: ICompositeActivity[]; } diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index be07132e..779b309c 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { Action, Separator } from 'vs/base/common/actions'; +import { Action, IAction, Separator } from 'vs/base/common/actions'; import * as dom from 'vs/base/browser/dom'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { dispose, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; @@ -374,14 +374,14 @@ export class CompositeOverflowActivityAction extends ActivityAction { } export class CompositeOverflowActivityActionViewItem extends ActivityActionViewItem { - private actions: Action[] = []; + private actions: IAction[] = []; constructor( action: ActivityAction, private getOverflowingComposites: () => { id: string, name?: string }[], private getActiveCompositeId: () => string | undefined, private getBadge: (compositeId: string) => IBadge, - private getCompositeOpenAction: (compositeId: string) => Action, + private getCompositeOpenAction: (compositeId: string) => IAction, colors: (theme: IColorTheme) => ICompositeBarColors, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IThemeService themeService: IThemeService @@ -404,7 +404,7 @@ export class CompositeOverflowActivityActionViewItem extends ActivityActionViewI }); } - private getActions(): Action[] { + private getActions(): IAction[] { return this.getOverflowingComposites().map(composite => { const action = this.getCompositeOpenAction(composite.id); action.checked = this.getActiveCompositeId() === action.id; @@ -457,9 +457,9 @@ export class CompositeActionViewItem extends ActivityActionViewItem { constructor( private compositeActivityAction: ActivityAction, - private toggleCompositePinnedAction: Action, - private compositeContextMenuActionsProvider: (compositeId: string) => ReadonlyArray, - private contextMenuActionsProvider: () => ReadonlyArray, + private toggleCompositePinnedAction: IAction, + private compositeContextMenuActionsProvider: (compositeId: string) => IAction[], + private contextMenuActionsProvider: () => IAction[], colors: (theme: IColorTheme) => ICompositeBarColors, icon: boolean, private dndHandler: ICompositeDragAndDrop, @@ -500,9 +500,14 @@ export class CompositeActionViewItem extends ActivityActionViewItem { return this.compositeActivity; } - private getActivtyName(): string { + private getActivtyName(skipKeybinding = false): string { + let name = this.compositeActivityAction.activity.name; + if (skipKeybinding) { + return name; + } + const keybinding = this.compositeActivityAction.activity.keybindingId ? this.keybindingService.lookupKeybinding(this.compositeActivityAction.activity.keybindingId) : null; - return keybinding ? nls.localize('titleKeybinding', "{0} ({1})", this.compositeActivityAction.activity.name, keybinding.getLabel()) : this.compositeActivityAction.activity.name; + return keybinding ? nls.localize('titleKeybinding', "{0} ({1})", name, keybinding.getLabel()) : name; } render(container: HTMLElement): void { @@ -601,7 +606,7 @@ export class CompositeActionViewItem extends ActivityActionViewItem { } private showContextMenu(container: HTMLElement): void { - const actions: Action[] = [this.toggleCompositePinnedAction]; + const actions: IAction[] = [this.toggleCompositePinnedAction]; const compositeContextMenuActions = this.compositeContextMenuActionsProvider(this.activity.id); if (compositeContextMenuActions.length) { @@ -615,10 +620,10 @@ export class CompositeActionViewItem extends ActivityActionViewItem { const isPinned = this.compositeBar.isPinned(this.activity.id); if (isPinned) { - this.toggleCompositePinnedAction.label = nls.localize('hide', "Hide"); + this.toggleCompositePinnedAction.label = nls.localize('hide', "Hide '{0}'", this.getActivtyName(true)); this.toggleCompositePinnedAction.checked = false; } else { - this.toggleCompositePinnedAction.label = nls.localize('keep', "Keep"); + this.toggleCompositePinnedAction.label = nls.localize('keep', "Keep '{0}'", this.getActivtyName(true)); } const otherActions = this.contextMenuActionsProvider(); diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 1350ddfe..13fe045e 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -327,10 +327,6 @@ export abstract class CompositePart extends Part { const primaryActions: IAction[] = composite?.getActions().slice(0) || []; const secondaryActions: IAction[] = composite?.getSecondaryActions().slice(0) || []; - // From Part - primaryActions.push(...this.getActions()); - secondaryActions.push(...this.getSecondaryActions()); - // Update context const toolBar = assertIsDefined(this.toolBar); toolBar.context = this.actionsContextProvider(); @@ -471,14 +467,6 @@ export abstract class CompositePart extends Part { return compositeItem ? compositeItem.progress : undefined; } - protected getActions(): ReadonlyArray { - return []; - } - - protected getSecondaryActions(): ReadonlyArray { - return []; - } - protected getTitleAreaDropDownAnchorAlignment(): AnchorAlignment { return AnchorAlignment.RIGHT; } diff --git a/src/vs/workbench/browser/parts/dialogs/dialog.web.contribution.ts b/src/vs/workbench/browser/parts/dialogs/dialog.web.contribution.ts index b5fc1d1f..836303db 100644 --- a/src/vs/workbench/browser/parts/dialogs/dialog.web.contribution.ts +++ b/src/vs/workbench/browser/parts/dialogs/dialog.web.contribution.ts @@ -19,9 +19,9 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { Disposable } from 'vs/base/common/lifecycle'; export class DialogHandlerContribution extends Disposable implements IWorkbenchContribution { - private impl: IDialogHandler; + private readonly model: IDialogsModel; + private readonly impl: IDialogHandler; - private model: IDialogsModel; private currentDialog: IDialogViewItem | undefined; constructor( diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 541ea9b2..026581b2 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -6,19 +6,13 @@ import * as dom from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { BreadcrumbsItem, BreadcrumbsWidget, IBreadcrumbsItemEvent } from 'vs/base/browser/ui/breadcrumbs/breadcrumbsWidget'; -import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { tail } from 'vs/base/common/arrays'; import { timeout } from 'vs/base/common/async'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { combinedDisposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { extUri } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/breadcrumbscontrol'; -import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; -import { Range } from 'vs/editor/common/core/range'; -import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; -import { SymbolKinds } from 'vs/editor/common/modes'; -import { OutlineElement, OutlineGroup, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { localize } from 'vs/nls'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; @@ -28,35 +22,92 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView import { FileKind, IFileService, IFileStat } from 'vs/platform/files/common/files'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IListService, WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; +import { IListService, WorkbenchDataTree, WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { ColorIdentifier, ColorFunction } from 'vs/platform/theme/common/colorRegistry'; import { attachBreadcrumbsStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ResourceLabel } from 'vs/workbench/browser/labels'; import { BreadcrumbsConfig, IBreadcrumbsService } from 'vs/workbench/browser/parts/editor/breadcrumbs'; -import { BreadcrumbElement, EditorBreadcrumbsModel, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; -import { BreadcrumbsPicker, createBreadcrumbsPicker } from 'vs/workbench/browser/parts/editor/breadcrumbsPicker'; +import { BreadcrumbsModel, FileElement, OutlineElement2 } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; +import { BreadcrumbsFilePicker, BreadcrumbsOutlinePicker, BreadcrumbsPicker } from 'vs/workbench/browser/parts/editor/breadcrumbsPicker'; import { IEditorPartOptions, EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { ACTIVE_GROUP, ACTIVE_GROUP_TYPE, IEditorService, SIDE_GROUP, SIDE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { onDidChangeZoomLevel } from 'vs/base/browser/browser'; -import { withNullAsUndefined, withUndefinedAsNull } from 'vs/base/common/types'; import { ILabelService } from 'vs/platform/label/common/label'; -import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; -import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; import { CATEGORIES } from 'vs/workbench/common/actions'; +import { ITreeNode } from 'vs/base/browser/ui/tree/tree'; +import { IOutline } from 'vs/workbench/services/outline/browser/outline'; -class Item extends BreadcrumbsItem { +class OutlineItem extends BreadcrumbsItem { private readonly _disposables = new DisposableStore(); constructor( - readonly element: BreadcrumbElement, + readonly model: BreadcrumbsModel, + readonly element: OutlineElement2, + readonly options: IBreadcrumbsControlOptions + ) { + super(); + } + + dispose(): void { + this._disposables.dispose(); + } + + equals(other: BreadcrumbsItem): boolean { + if (!(other instanceof OutlineItem)) { + return false; + } + return this.element === other.element && + this.options.showFileIcons === other.options.showFileIcons && + this.options.showSymbolIcons === other.options.showSymbolIcons; + } + + render(container: HTMLElement): void { + const { element, outline } = this.element; + + if (element === outline) { + const element = dom.$('span', undefined, '…'); + container.appendChild(element); + return; + } + + const templateId = outline.config.delegate.getTemplateId(element); + const renderer = outline.config.renderers.find(renderer => renderer.templateId === templateId); + if (!renderer) { + container.innerText = '<>'; + return; + } + + const template = renderer.renderTemplate(container); + renderer.renderElement(>{ + element, + children: [], + depth: 0, + visibleChildrenCount: 0, + visibleChildIndex: 0, + collapsible: false, + collapsed: false, + visible: true, + filterData: undefined + }, 0, template, undefined); + + this._disposables.add(toDisposable(() => { renderer.disposeTemplate(template); })); + } + +} + +class FileItem extends BreadcrumbsItem { + + private readonly _disposables = new DisposableStore(); + + constructor( + readonly model: BreadcrumbsModel, + readonly element: FileElement, readonly options: IBreadcrumbsControlOptions, @IInstantiationService private readonly _instantiationService: IInstantiationService ) { @@ -68,59 +119,26 @@ class Item extends BreadcrumbsItem { } equals(other: BreadcrumbsItem): boolean { - if (!(other instanceof Item)) { + if (!(other instanceof FileItem)) { return false; } - if (this.element instanceof FileElement && other.element instanceof FileElement) { - return (extUri.isEqual(this.element.uri, other.element.uri) && - this.options.showFileIcons === other.options.showFileIcons && - this.options.showSymbolIcons === other.options.showSymbolIcons); - } - if (this.element instanceof TreeElement && other.element instanceof TreeElement) { - return this.element.id === other.element.id; - } - return false; + return (extUri.isEqual(this.element.uri, other.element.uri) && + this.options.showFileIcons === other.options.showFileIcons && + this.options.showSymbolIcons === other.options.showSymbolIcons); + } render(container: HTMLElement): void { - if (this.element instanceof FileElement) { - // file/folder - let label = this._instantiationService.createInstance(ResourceLabel, container, {}); - label.element.setFile(this.element.uri, { - hidePath: true, - hideIcon: this.element.kind === FileKind.FOLDER || !this.options.showFileIcons, - fileKind: this.element.kind, - fileDecorations: { colors: this.options.showDecorationColors, badges: false }, - }); - container.classList.add(FileKind[this.element.kind].toLowerCase()); - this._disposables.add(label); - - } else if (this.element instanceof OutlineModel) { - // has outline element but not in one - let label = document.createElement('div'); - label.innerText = '\u2026'; - label.className = 'hint-more'; - container.appendChild(label); - - } else if (this.element instanceof OutlineGroup) { - // provider - let label = new IconLabel(container); - label.setLabel(this.element.label); - this._disposables.add(label); - - } else if (this.element instanceof OutlineElement) { - // symbol - if (this.options.showSymbolIcons) { - let icon = document.createElement('div'); - icon.className = SymbolKinds.toCssClassName(this.element.symbol.kind); - container.appendChild(icon); - container.classList.add('shows-symbol-icon'); - } - let label = new IconLabel(container); - let title = this.element.symbol.name.replace(/\r|\n|\r\n/g, '\u23CE'); - label.setLabel(title); - this._disposables.add(label); - } + // file/folder + let label = this._instantiationService.createInstance(ResourceLabel, container, {}); + label.element.setFile(this.element.uri, { + hidePath: true, + hideIcon: this.element.kind === FileKind.FOLDER || !this.options.showFileIcons, + fileKind: this.element.kind, + fileDecorations: { colors: this.options.showDecorationColors, badges: false }, + }); + container.classList.add(FileKind[this.element.kind].toLowerCase()); + this._disposables.add(label); } } @@ -170,26 +188,23 @@ export class BreadcrumbsControl { private readonly _editorGroup: IEditorGroupView, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IContextViewService private readonly _contextViewService: IContextViewService, - @IEditorService private readonly _editorService: IEditorService, - @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, - @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IThemeService private readonly _themeService: IThemeService, @IQuickInputService private readonly _quickInputService: IQuickInputService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @ITextResourceConfigurationService private readonly _textResourceConfigurationService: ITextResourceConfigurationService, @IFileService private readonly _fileService: IFileService, @ITelemetryService private readonly _telemetryService: ITelemetryService, + @IEditorService private readonly _editorService: IEditorService, @ILabelService private readonly _labelService: ILabelService, + @IConfigurationService configurationService: IConfigurationService, @IBreadcrumbsService breadcrumbsService: IBreadcrumbsService, ) { this.domNode = document.createElement('div'); this.domNode.classList.add('breadcrumbs-control'); dom.append(container, this.domNode); - this._cfUseQuickPick = BreadcrumbsConfig.UseQuickPick.bindTo(_configurationService); - this._cfShowIcons = BreadcrumbsConfig.Icons.bindTo(_configurationService); - this._cfTitleScrollbarSizing = BreadcrumbsConfig.TitleScrollbarSizing.bindTo(_configurationService); + this._cfUseQuickPick = BreadcrumbsConfig.UseQuickPick.bindTo(configurationService); + this._cfShowIcons = BreadcrumbsConfig.Icons.bindTo(configurationService); + this._cfTitleScrollbarSizing = BreadcrumbsConfig.TitleScrollbarSizing.bindTo(configurationService); const sizing = this._cfTitleScrollbarSizing.getValue() ?? 'default'; this._widget = new BreadcrumbsWidget(this.domNode, BreadcrumbsControl.SCROLLBAR_SIZES[sizing]); @@ -256,14 +271,11 @@ export class BreadcrumbsControl { this._ckBreadcrumbsVisible.set(true); this._ckBreadcrumbsPossible.set(true); - const editor = this._getActiveCodeEditor(); - const model = new EditorBreadcrumbsModel( + const model = this._instantiationService.createInstance(BreadcrumbsModel, fileInfoUri ?? uri, - uri, editor, - this._configurationService, - this._textResourceConfigurationService, - this._workspaceService + this._editorGroup.activeEditorPane ); + this.domNode.classList.toggle('relative-path', model.isRelative()); this.domNode.classList.toggle('backslash-path', this._labelService.getSeparator(uri.scheme, uri.authority) === '\\'); @@ -274,7 +286,7 @@ export class BreadcrumbsControl { showFileIcons: this._options.showFileIcons && showIcons, showSymbolIcons: this._options.showSymbolIcons && showIcons }; - const items = model.getElements().map(element => new Item(element, options, this._instantiationService)); + const items = model.getElements().map(element => element instanceof FileElement ? new FileItem(model, element, options, this._instantiationService) : new OutlineItem(model, element, options)); this._widget.setItems(items); this._widget.reveal(items[items.length - 1]); }; @@ -298,7 +310,7 @@ export class BreadcrumbsControl { this._breadcrumbsDisposables.add({ dispose: () => { if (this._breadcrumbsPickerShowing) { - this._contextViewService.hideContextView(this); + this._contextViewService.hideContextView({ source: this }); } } }); @@ -306,20 +318,6 @@ export class BreadcrumbsControl { return true; } - private _getActiveCodeEditor(): ICodeEditor | undefined { - if (!this._editorGroup.activeEditorPane) { - return undefined; - } - let control = this._editorGroup.activeEditorPane.getControl(); - let editor: ICodeEditor | undefined; - if (isCodeEditor(control)) { - editor = control as ICodeEditor; - } else if (isDiffEditor(control)) { - editor = control.getModifiedEditor(); - } - return editor; - } - private _onFocusEvent(event: IBreadcrumbsItemEvent): void { if (event.item && this._breadcrumbsPickerShowing) { this._breadcrumbsPickerIgnoreOnceItem = undefined; @@ -339,13 +337,12 @@ export class BreadcrumbsControl { return; } - const { element } = event.item as Item; + const { element } = event.item as FileItem | OutlineItem; this._editorGroup.focus(); - type BreadcrumbSelectClassification = { - type: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - }; - this._telemetryService.publicLog2<{ type: string }, BreadcrumbSelectClassification>('breadcrumbs/select', { type: element instanceof TreeElement ? 'symbol' : 'file' }); + type BreadcrumbSelect = { type: string }; + type BreadcrumbSelectClassification = { type: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; }; + this._telemetryService.publicLog2('breadcrumbs/select', { type: event.item instanceof OutlineItem ? 'symbol' : 'file' }); const group = this._getEditorGroup(event.payload); if (group !== undefined) { @@ -360,64 +357,31 @@ export class BreadcrumbsControl { // using quick pick this._widget.setFocused(undefined); this._widget.setSelection(undefined); - this._quickInputService.quickAccess.show(element instanceof TreeElement ? '@' : ''); + this._quickInputService.quickAccess.show(element instanceof OutlineElement2 ? '@' : ''); return; } // show picker let picker: BreadcrumbsPicker; let pickerAnchor: { x: number; y: number }; - let editor = this._getActiveCodeEditor(); - let editorDecorations: string[] = []; - let editorViewState: ICodeEditorViewState | undefined; + + interface IHideData { didPick?: boolean, source?: BreadcrumbsControl } this._contextViewService.showContextView({ render: (parent: HTMLElement) => { - picker = createBreadcrumbsPicker(this._instantiationService, parent, element); - let selectListener = picker.onDidPickElement(data => { - if (data.target) { - editorViewState = undefined; - } - this._contextViewService.hideContextView(this); + if (event.item instanceof FileItem) { + picker = this._instantiationService.createInstance(BreadcrumbsFilePicker, parent, event.item.model.resource); + } else if (event.item instanceof OutlineItem) { + picker = this._instantiationService.createInstance(BreadcrumbsOutlinePicker, parent, event.item.model.resource); + } - const group = (picker.useAltAsMultipleSelectionModifier && (data.browserEvent as MouseEvent).metaKey) || (!picker.useAltAsMultipleSelectionModifier && (data.browserEvent as MouseEvent).altKey) - ? SIDE_GROUP - : ACTIVE_GROUP; - - this._revealInEditor(event, data.target, group, (data.browserEvent as MouseEvent).button === 1); - /* __GDPR__ - "breadcrumbs/open" : { - "type": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this._telemetryService.publicLog('breadcrumbs/open', { type: !data ? 'nothing' : data.target instanceof TreeElement ? 'symbol' : 'file' }); - }); - let focusListener = picker.onDidFocusElement(data => { - if (!editor || !(data.target instanceof OutlineElement)) { - return; - } - if (!editorViewState) { - editorViewState = withNullAsUndefined(editor.saveViewState()); - } - const { symbol } = data.target; - editor.revealRangeInCenterIfOutsideViewport(symbol.range, ScrollType.Smooth); - editorDecorations = editor.deltaDecorations(editorDecorations, [{ - range: symbol.range, - options: { - className: 'rangeHighlight', - isWholeLine: true - } - }]); - }); - - let zoomListener = onDidChangeZoomLevel(() => { - this._contextViewService.hideContextView(this); - }); + let selectListener = picker.onWillPickElement(() => this._contextViewService.hideContextView({ source: this, didPick: true })); + let zoomListener = onDidChangeZoomLevel(() => this._contextViewService.hideContextView({ source: this })); let focusTracker = dom.trackFocus(parent); let blurListener = focusTracker.onDidBlur(() => { this._breadcrumbsPickerIgnoreOnceItem = this._widget.isDOMFocused() ? event.item : undefined; - this._contextViewService.hideContextView(this); + this._contextViewService.hideContextView({ source: this }); }); this._breadcrumbsPickerShowing = true; @@ -426,7 +390,6 @@ export class BreadcrumbsControl { return combinedDisposable( picker, selectListener, - focusListener, zoomListener, focusTracker, blurListener @@ -465,19 +428,17 @@ export class BreadcrumbsControl { } return pickerAnchor; }, - onHide: (data) => { - if (editor) { - editor.deltaDecorations(editorDecorations, []); - if (editorViewState) { - editor.restoreViewState(editorViewState); - } + onHide: (data?: IHideData) => { + if (!data?.didPick) { + picker.restoreViewState(); } this._breadcrumbsPickerShowing = false; this._updateCkBreadcrumbsActive(); - if (data === this) { + if (data?.source === this) { this._widget.setFocused(undefined); this._widget.setSelection(undefined); } + picker.dispose(); } }); } @@ -487,11 +448,11 @@ export class BreadcrumbsControl { this._ckBreadcrumbsActive.set(value); } - private _revealInEditor(event: IBreadcrumbsItemEvent, element: BreadcrumbElement, group: SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | undefined, pinned: boolean = false): void { + private async _revealInEditor(event: IBreadcrumbsItemEvent, element: FileElement | OutlineElement2, group: SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | undefined, pinned: boolean = false): Promise { + if (element instanceof FileElement) { if (element.kind === FileKind.FILE) { - // open file in any editor - this._editorService.openEditor({ resource: element.uri, options: { pinned } }, group); + await this._editorService.openEditor({ resource: element.uri, options: { pinned } }, group); } else { // show next picker let items = this._widget.getItems(); @@ -499,20 +460,8 @@ export class BreadcrumbsControl { this._widget.setFocused(items[idx + 1]); this._widget.setSelection(items[idx + 1], BreadcrumbsControl.Payload_Pick); } - - } else if (element instanceof OutlineElement) { - // open symbol in code editor - const model = OutlineModel.get(element); - if (model) { - this._codeEditorService.openCodeEditor({ - resource: model.uri, - options: { - selection: Range.collapseToStart(element.symbol.selectionRange), - selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport, - pinned - } - }, withUndefinedAsNull(this._getActiveCodeEditor()), group === SIDE_GROUP); - } + } else { + element.outline.reveal(element, { pinned }, group === SIDE_GROUP); } } @@ -744,29 +693,29 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ handler(accessor) { const editors = accessor.get(IEditorService); const lists = accessor.get(IListService); - const element = lists.lastFocusedList ? lists.lastFocusedList.getFocus()[0] : undefined; - if (element instanceof OutlineElement) { - const outlineElement = OutlineModel.get(element); - if (!outlineElement) { - return undefined; - } - // open symbol in editor - return editors.openEditor({ - resource: outlineElement.uri, - options: { selection: Range.collapseToStart(element.symbol.selectionRange), pinned: true } - }, SIDE_GROUP); + const tree = lists.lastFocusedList; + if (!(tree instanceof WorkbenchDataTree)) { + return; + } - } else if (element && URI.isUri(element.resource)) { - // open file in editor + const element = tree.getFocus()[0]; + + if (URI.isUri((element)?.resource)) { + // IFileStat: open file in editor return editors.openEditor({ - resource: element.resource, + resource: (element).resource, options: { pinned: true } }, SIDE_GROUP); + } - } else { - // ignore - return undefined; + // IOutline: check if this the outline and iff so reveal element + const input = tree.getInput(); + if (input && typeof (>input).outlineKind === 'string') { + return (>input).reveal(element, { + pinned: true, + preserveFocus: false + }, true); } } }); diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts index 01ff3916..4364969d 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts @@ -3,27 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { equals } from 'vs/base/common/arrays'; -import { TimeoutTimer } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { isEqual, dirname } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IPosition } from 'vs/editor/common/core/position'; -import { DocumentSymbolProviderRegistry } from 'vs/editor/common/modes'; -import { OutlineElement, OutlineGroup, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { Schemas } from 'vs/base/common/network'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; import { FileKind } from 'vs/platform/files/common/files'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { OutlineFilter } from 'vs/editor/contrib/documentSymbols/outlineTree'; -import { ITextModel } from 'vs/editor/common/model'; -import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { IOutline, IOutlineService, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; +import { IEditorPane } from 'vs/workbench/common/editor'; export class FileElement { constructor( @@ -32,11 +25,16 @@ export class FileElement { ) { } } -export type BreadcrumbElement = FileElement | OutlineModel | OutlineGroup | OutlineElement; - type FileInfo = { path: FileElement[], folder?: IWorkspaceFolder }; -export class EditorBreadcrumbsModel { +export class OutlineElement2 { + constructor( + readonly element: IOutline | any, + readonly outline: IOutline + ) { } +} + +export class BreadcrumbsModel { private readonly _disposables = new DisposableStore(); private readonly _fileInfo: FileInfo; @@ -45,28 +43,31 @@ export class EditorBreadcrumbsModel { private readonly _cfgFilePath: BreadcrumbsConfig<'on' | 'off' | 'last'>; private readonly _cfgSymbolPath: BreadcrumbsConfig<'on' | 'off' | 'last'>; - private _outlineElements: Array = []; - private _outlineDisposables = new DisposableStore(); + private readonly _currentOutline = new MutableDisposable>(); + private readonly _outlineDisposables = new DisposableStore(); private readonly _onDidUpdate = new Emitter(); readonly onDidUpdate: Event = this._onDidUpdate.event; constructor( - fileInfoUri: URI, - private readonly _uri: URI, - private readonly _editor: ICodeEditor | undefined, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @ITextResourceConfigurationService private readonly _textResourceConfigurationService: ITextResourceConfigurationService, - @IWorkspaceContextService workspaceService: IWorkspaceContextService, + readonly resource: URI, + editor: IEditorPane | undefined, + @IConfigurationService configurationService: IConfigurationService, + @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, + @IOutlineService private readonly _outlineService: IOutlineService, ) { - this._cfgEnabled = BreadcrumbsConfig.IsEnabled.bindTo(_configurationService); - this._cfgFilePath = BreadcrumbsConfig.FilePath.bindTo(_configurationService); - this._cfgSymbolPath = BreadcrumbsConfig.SymbolPath.bindTo(_configurationService); + this._cfgEnabled = BreadcrumbsConfig.IsEnabled.bindTo(configurationService); + this._cfgFilePath = BreadcrumbsConfig.FilePath.bindTo(configurationService); + this._cfgSymbolPath = BreadcrumbsConfig.SymbolPath.bindTo(configurationService); this._disposables.add(this._cfgFilePath.onDidChange(_ => this._onDidUpdate.fire(this))); this._disposables.add(this._cfgSymbolPath.onDidChange(_ => this._onDidUpdate.fire(this))); - this._fileInfo = EditorBreadcrumbsModel._initFilePathInfo(fileInfoUri, workspaceService); - this._bindToEditor(); + this._fileInfo = this._initFilePathInfo(resource); + + if (editor) { + this._bindToEditor(editor); + this._disposables.add(_outlineService.onDidChange(() => this._bindToEditor(editor))); + } this._onDidUpdate.fire(this); } @@ -74,6 +75,7 @@ export class EditorBreadcrumbsModel { this._cfgEnabled.dispose(); this._cfgFilePath.dispose(); this._cfgSymbolPath.dispose(); + this._currentOutline.dispose(); this._outlineDisposables.dispose(); this._disposables.dispose(); this._onDidUpdate.dispose(); @@ -83,8 +85,8 @@ export class EditorBreadcrumbsModel { return Boolean(this._fileInfo.folder); } - getElements(): ReadonlyArray { - let result: BreadcrumbElement[] = []; + getElements(): ReadonlyArray { + let result: (FileElement | OutlineElement2)[] = []; // file path elements if (this._cfgFilePath.getValue() === 'on') { @@ -93,17 +95,27 @@ export class EditorBreadcrumbsModel { result = result.concat(this._fileInfo.path.slice(-1)); } - // symbol path elements - if (this._cfgSymbolPath.getValue() === 'on') { - result = result.concat(this._outlineElements); - } else if (this._cfgSymbolPath.getValue() === 'last' && this._outlineElements.length > 0) { - result = result.concat(this._outlineElements.slice(-1)); + if (this._cfgSymbolPath.getValue() === 'off') { + return result; + } + + if (!this._currentOutline.value) { + return result; + } + + const breadcrumbsElements = this._currentOutline.value.config.breadcrumbsDataSource.getBreadcrumbElements(); + for (let i = this._cfgSymbolPath.getValue() === 'last' && breadcrumbsElements.length > 0 ? breadcrumbsElements.length - 1 : 0; i < breadcrumbsElements.length; i++) { + result.push(new OutlineElement2(breadcrumbsElements[i], this._currentOutline.value)); + } + + if (breadcrumbsElements.length === 0 && !this._currentOutline.value.isEmpty) { + result.push(new OutlineElement2(this._currentOutline.value, this._currentOutline.value)); } return result; } - private static _initFilePathInfo(uri: URI, workspaceService: IWorkspaceContextService): FileInfo { + private _initFilePathInfo(uri: URI): FileInfo { if (uri.scheme === Schemas.untitled) { return { @@ -113,7 +125,7 @@ export class EditorBreadcrumbsModel { } let info: FileInfo = { - folder: withNullAsUndefined(workspaceService.getWorkspaceFolder(uri)), + folder: withNullAsUndefined(this._workspaceService.getWorkspaceFolder(uri)), path: [] }; @@ -130,181 +142,33 @@ export class EditorBreadcrumbsModel { } } - if (info.folder && workspaceService.getWorkbenchState() === WorkbenchState.WORKSPACE) { + if (info.folder && this._workspaceService.getWorkbenchState() === WorkbenchState.WORKSPACE) { info.path.unshift(new FileElement(info.folder.uri, FileKind.ROOT_FOLDER)); } return info; } - private _bindToEditor(): void { - if (!this._editor) { - return; - } - // update as language, model, providers changes - this._disposables.add(DocumentSymbolProviderRegistry.onDidChange(_ => this._updateOutline())); - this._disposables.add(this._editor.onDidChangeModel(_ => this._updateOutline())); - this._disposables.add(this._editor.onDidChangeModelLanguage(_ => this._updateOutline())); - - // update when config changes (re-render) - this._disposables.add(this._configurationService.onDidChangeConfiguration(e => { - if (!this._cfgEnabled.getValue()) { - // breadcrumbs might be disabled (also via a setting/config) and that is - // something we must check before proceeding. - return; - } - if (e.affectsConfiguration('breadcrumbs')) { - this._updateOutline(true); - return; - } - if (this._editor && this._editor.getModel()) { - const editorModel = this._editor.getModel() as ITextModel; - const languageName = editorModel.getLanguageIdentifier().language; - - // Checking for changes in the current language override config. - // We can't be more specific than this because the ConfigurationChangeEvent(e) only includes the first part of the root path - if (e.affectsConfiguration(`[${languageName}]`)) { - this._updateOutline(true); - } - } - })); - - - // update soon'ish as model content change - const updateSoon = new TimeoutTimer(); - this._disposables.add(updateSoon); - this._disposables.add(this._editor.onDidChangeModelContent(_ => { - const timeout = OutlineModel.getRequestDelay(this._editor!.getModel()); - updateSoon.cancelAndSet(() => this._updateOutline(true), timeout); - })); - this._updateOutline(); - - // stop when editor dies - this._disposables.add(this._editor.onDidDispose(() => this._outlineDisposables.clear())); - } - - private _updateOutline(didChangeContent?: boolean): void { - + private _bindToEditor(editor: IEditorPane): void { + const newCts = new CancellationTokenSource(); + this._currentOutline.clear(); this._outlineDisposables.clear(); - if (!didChangeContent) { - this._updateOutlineElements([]); - } + this._outlineDisposables.add(toDisposable(() => newCts.dispose(true))); - const editor = this._editor!; - - const buffer = editor.getModel(); - if (!buffer || !DocumentSymbolProviderRegistry.has(buffer) || !isEqual(buffer.uri, this._uri)) { - return; - } - - const source = new CancellationTokenSource(); - const versionIdThen = buffer.getVersionId(); - const timeout = new TimeoutTimer(); - - this._outlineDisposables.add({ - dispose: () => { - source.dispose(true); - timeout.dispose(); + this._outlineService.createOutline(editor, OutlineTarget.Breadcrumbs, newCts.token).then(outline => { + if (newCts.token.isCancellationRequested) { + // cancelled: dispose new outline and reset + outline?.dispose(); + outline = undefined; } - }); - - OutlineModel.create(buffer, source.token).then(model => { - if (source.token.isCancellationRequested) { - // cancelled -> do nothing - return; + this._currentOutline.value = outline; + this._onDidUpdate.fire(this); + if (outline) { + this._outlineDisposables.add(outline.onDidChange(() => this._onDidUpdate.fire(this))); } - if (TreeElement.empty(model)) { - // empty -> no outline elements - this._updateOutlineElements([]); - } else { - // copy the model - model = model.adopt(); - - this._updateOutlineElements(this._getOutlineElements(model, editor.getPosition())); - this._outlineDisposables.add(editor.onDidChangeCursorPosition(_ => { - timeout.cancelAndSet(() => { - if (!buffer.isDisposed() && versionIdThen === buffer.getVersionId() && editor.getModel()) { - this._updateOutlineElements(this._getOutlineElements(model, editor.getPosition())); - } - }, 150); - })); - } }).catch(err => { - this._updateOutlineElements([]); + this._onDidUpdate.fire(this); onUnexpectedError(err); }); } - - private _getOutlineElements(model: OutlineModel, position: IPosition | null): Array { - if (!model || !position) { - return []; - } - let item: OutlineGroup | OutlineElement | undefined = model.getItemEnclosingPosition(position); - if (!item) { - return this._getOutlineElementsRoot(model); - } - let chain: Array = []; - while (item) { - chain.push(item); - let parent: any = item.parent; - if (parent instanceof OutlineModel) { - break; - } - if (parent instanceof OutlineGroup && parent.parent && parent.parent.children.size === 1) { - break; - } - item = parent; - } - let result: Array = []; - for (let i = chain.length - 1; i >= 0; i--) { - let element = chain[i]; - if (this._isFiltered(element)) { - break; - } - result.push(element); - } - if (result.length === 0) { - return this._getOutlineElementsRoot(model); - } - return result; - } - - private _getOutlineElementsRoot(model: OutlineModel): (OutlineModel | OutlineGroup | OutlineElement)[] { - for (const child of model.children.values()) { - if (!this._isFiltered(child)) { - return [model]; - } - } - return []; - } - - private _isFiltered(element: TreeElement): boolean { - if (element instanceof OutlineElement) { - const key = `breadcrumbs.${OutlineFilter.kindToConfigName[element.symbol.kind]}`; - let uri: URI | undefined; - if (this._editor && this._editor.getModel()) { - const model = this._editor.getModel() as ITextModel; - uri = model.uri; - } - return !this._textResourceConfigurationService.getValue(uri, key); - } - return false; - } - - private _updateOutlineElements(elements: Array): void { - if (!equals(elements, this._outlineElements, EditorBreadcrumbsModel._outlineElementEquals)) { - this._outlineElements = elements; - this._onDidUpdate.fire(this); - } - } - - private static _outlineElementEquals(a: OutlineModel | OutlineGroup | OutlineElement, b: OutlineModel | OutlineGroup | OutlineElement): boolean { - if (a === b) { - return true; - } else if (!a || !b) { - return false; - } else { - return a.id === b.id; - } - } } diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts index 001398ec..223380e6 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts @@ -8,34 +8,30 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { createMatches, FuzzyScore } from 'vs/base/common/filters'; import * as glob from 'vs/base/common/glob'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IDisposable, DisposableStore, MutableDisposable, Disposable } from 'vs/base/common/lifecycle'; import { posix } from 'vs/base/common/path'; import { basename, dirname, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/breadcrumbscontrol'; -import { OutlineElement, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; -import { IConfigurationService, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { FileKind, IFileService, IFileStat } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { WorkbenchDataTree, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { breadcrumbsPickerBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; -import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { isWorkspace, isWorkspaceFolder, IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ResourceLabels, IResourceLabel, DEFAULT_LABELS_CONTAINER } from 'vs/workbench/browser/labels'; import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; -import { BreadcrumbElement, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; +import { OutlineElement2, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; import { IAsyncDataSource, ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, ITreeSorter } from 'vs/base/browser/ui/tree/tree'; -import { OutlineVirtualDelegate, OutlineGroupRenderer, OutlineElementRenderer, OutlineItemComparator, OutlineIdentityProvider, OutlineNavigationLabelProvider, OutlineDataSource, OutlineSortOrder, OutlineFilter, OutlineAccessibilityProvider } from 'vs/editor/contrib/documentSymbols/outlineTree'; import { IIdentityProvider, IListVirtualDelegate, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list'; import { IFileIconTheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { IModeService } from 'vs/editor/common/services/modeService'; import { localize } from 'vs/nls'; - -export function createBreadcrumbsPicker(instantiationService: IInstantiationService, parent: HTMLElement, element: BreadcrumbElement): BreadcrumbsPicker { - return element instanceof FileElement - ? instantiationService.createInstance(BreadcrumbsFilePicker, parent) - : instantiationService.createInstance(BreadcrumbsOutlinePicker, parent); -} +import { IOutline, IOutlineComparator } from 'vs/workbench/services/outline/browser/outline'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; interface ILayoutInfo { maxHeight: number; @@ -62,17 +58,18 @@ export abstract class BreadcrumbsPicker { protected _fakeEvent = new UIEvent('fakeEvent'); protected _layoutInfo!: ILayoutInfo; - private readonly _onDidPickElement = new Emitter(); - readonly onDidPickElement: Event = this._onDidPickElement.event; + protected readonly _onWillPickElement = new Emitter(); + readonly onWillPickElement: Event = this._onWillPickElement.event; - private readonly _onDidFocusElement = new Emitter(); - readonly onDidFocusElement: Event = this._onDidFocusElement.event; + private readonly _previewDispoables = new MutableDisposable(); constructor( parent: HTMLElement, + protected resource: URI, @IInstantiationService protected readonly _instantiationService: IInstantiationService, @IThemeService protected readonly _themeService: IThemeService, @IConfigurationService protected readonly _configurationService: IConfigurationService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { this._domNode = document.createElement('div'); this._domNode.className = 'monaco-breadcrumbs-picker show-file-icons'; @@ -81,12 +78,13 @@ export abstract class BreadcrumbsPicker { dispose(): void { this._disposables.dispose(); - this._onDidPickElement.dispose(); - this._onDidFocusElement.dispose(); - this._tree.dispose(); + this._previewDispoables.dispose(); + this._onWillPickElement.dispose(); + this._domNode.remove(); + setTimeout(() => this._tree.dispose(), 0); // tree cannot be disposed while being opened... } - show(input: any, maxHeight: number, width: number, arrowSize: number, arrowOffset: number): void { + async show(input: any, maxHeight: number, width: number, arrowSize: number, arrowOffset: number): Promise { const theme = this._themeService.getColorTheme(); const color = theme.getColor(breadcrumbsPickerBackground); @@ -103,33 +101,33 @@ export abstract class BreadcrumbsPicker { this._domNode.appendChild(this._treeContainer); this._layoutInfo = { maxHeight, width, arrowSize, arrowOffset, inputHeight: 0 }; - this._tree = this._createTree(this._treeContainer); + this._tree = this._createTree(this._treeContainer, input); - this._disposables.add(this._tree.onDidChangeSelection(e => { - if (e.browserEvent !== this._fakeEvent) { - const target = this._getTargetFromEvent(e.elements[0]); - if (target) { - setTimeout(_ => {// need to debounce here because this disposes the tree and the tree doesn't like to be disposed on click - this._onDidPickElement.fire({ target, browserEvent: e.browserEvent || new UIEvent('fake') }); - }, 0); - } + this._disposables.add(this._tree.onDidOpen(async e => { + const { element, editorOptions, sideBySide } = e; + const didReveal = await this._revealElement(element, { ...editorOptions, preserveFocus: false }, sideBySide); + if (!didReveal) { + return; } + // send telemetry + interface OpenEvent { type: string } + interface OpenEventGDPR { type: { classification: 'SystemMetaData', purpose: 'FeatureInsight' } } + this._telemetryService.publicLog2('breadcrumbs/open', { type: element instanceof OutlineElement2 ? 'symbol' : 'file' }); })); this._disposables.add(this._tree.onDidChangeFocus(e => { - const target = this._getTargetFromEvent(e.elements[0]); - if (target) { - this._onDidFocusElement.fire({ target, browserEvent: e.browserEvent || new UIEvent('fake') }); - } + this._previewDispoables.value = this._previewElement(e.elements[0]); })); this._disposables.add(this._tree.onDidChangeContentHeight(() => { this._layout(); })); this._domNode.focus(); - - this._setInput(input).then(() => { + try { + await this._setInput(input); this._layout(); - }).catch(onUnexpectedError); + } catch (err) { + onUnexpectedError(err); + } } protected _layout(): void { @@ -146,16 +144,15 @@ export abstract class BreadcrumbsPicker { this._treeContainer.style.height = `${treeHeight}px`; this._treeContainer.style.width = `${this._layoutInfo.width}px`; this._tree.layout(treeHeight, this._layoutInfo.width); - } - get useAltAsMultipleSelectionModifier() { - return this._tree.useAltAsMultipleSelectionModifier; - } + restoreViewState(): void { } + + protected abstract _setInput(element: FileElement | OutlineElement2): Promise; + protected abstract _createTree(container: HTMLElement, input: any): Tree; + protected abstract _previewElement(element: any): IDisposable; + protected abstract _revealElement(element: any, options: IEditorOptions, sideBySide: boolean): Promise; - protected abstract _setInput(element: BreadcrumbElement): Promise; - protected abstract _createTree(container: HTMLElement): Tree; - protected abstract _getTargetFromEvent(element: any): any | undefined; } //#region - Files @@ -173,9 +170,9 @@ class FileIdentityProvider implements IIdentityProvider { - private readonly _parents = new WeakMap(); - constructor( @IFileService private readonly _fileService: IFileService, ) { } hasChildren(element: IWorkspace | URI | IWorkspaceFolder | IFileStat): boolean { return URI.isUri(element) - || IWorkspace.isIWorkspace(element) - || IWorkspaceFolder.isIWorkspaceFolder(element) + || isWorkspace(element) + || isWorkspaceFolder(element) || element.isDirectory; } - getChildren(element: IWorkspace | URI | IWorkspaceFolder | IFileStat): Promise<(IWorkspaceFolder | IFileStat)[]> { - - if (IWorkspace.isIWorkspace(element)) { - return Promise.resolve(element.folders).then(folders => { - for (let child of folders) { - this._parents.set(element, child); - } - return folders; - }); + async getChildren(element: IWorkspace | URI | IWorkspaceFolder | IFileStat): Promise<(IWorkspaceFolder | IFileStat)[]> { + if (isWorkspace(element)) { + return element.folders; } let uri: URI; - if (IWorkspaceFolder.isIWorkspaceFolder(element)) { + if (isWorkspaceFolder(element)) { uri = element.uri; } else if (URI.isUri(element)) { uri = element; } else { uri = element.resource; } - return this._fileService.resolve(uri).then(stat => { - for (const child of stat.children || []) { - this._parents.set(stat, child); - } - return stat.children || []; - }); + const stat = await this._fileService.resolve(uri); + return stat.children ?? []; } } @@ -245,7 +230,7 @@ class FileRenderer implements ITreeRenderer { } filter(element: IWorkspaceFolder | IFileStat, _parentVisibility: TreeVisibility): boolean { - if (IWorkspaceFolder.isIWorkspaceFolder(element)) { + if (isWorkspaceFolder(element)) { // not a file return true; } @@ -345,7 +330,7 @@ class FileFilter implements ITreeFilter { export class FileSorter implements ITreeSorter { compare(a: IFileStat | IWorkspaceFolder, b: IFileStat | IWorkspaceFolder): number { - if (IWorkspaceFolder.isIWorkspaceFolder(a) && IWorkspaceFolder.isIWorkspaceFolder(b)) { + if (isWorkspaceFolder(a) && isWorkspaceFolder(b)) { return a.index - b.index; } if ((a as IFileStat).isDirectory === (b as IFileStat).isDirectory) { @@ -363,12 +348,15 @@ export class BreadcrumbsFilePicker extends BreadcrumbsPicker { constructor( parent: HTMLElement, + protected resource: URI, @IInstantiationService instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @IConfigurationService configService: IConfigurationService, @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, + @IEditorService private readonly _editorService: IEditorService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super(parent, instantiationService, themeService, configService); + super(parent, resource, instantiationService, themeService, configService, telemetryService); } _createTree(container: HTMLElement) { @@ -406,7 +394,7 @@ export class BreadcrumbsFilePicker extends BreadcrumbsPicker { }); } - _setInput(element: BreadcrumbElement): Promise { + async _setInput(element: FileElement | OutlineElement2): Promise { const { uri, kind } = (element as FileElement); let input: IWorkspace | URI; if (kind === FileKind.ROOT_FOLDER) { @@ -416,115 +404,120 @@ export class BreadcrumbsFilePicker extends BreadcrumbsPicker { } const tree = this._tree as WorkbenchAsyncDataTree; - return tree.setInput(input).then(() => { - let focusElement: IWorkspaceFolder | IFileStat | undefined; - for (const { element } of tree.getNode().children) { - if (IWorkspaceFolder.isIWorkspaceFolder(element) && isEqual(element.uri, uri)) { - focusElement = element; - break; - } else if (isEqual((element as IFileStat).resource, uri)) { - focusElement = element as IFileStat; - break; - } + await tree.setInput(input); + let focusElement: IWorkspaceFolder | IFileStat | undefined; + for (const { element } of tree.getNode().children) { + if (isWorkspaceFolder(element) && isEqual(element.uri, uri)) { + focusElement = element; + break; + } else if (isEqual((element as IFileStat).resource, uri)) { + focusElement = element as IFileStat; + break; } - if (focusElement) { - tree.reveal(focusElement, 0.5); - tree.setFocus([focusElement], this._fakeEvent); - } - tree.domFocus(); - }); + } + if (focusElement) { + tree.reveal(focusElement, 0.5); + tree.setFocus([focusElement], this._fakeEvent); + } + tree.domFocus(); } - protected _getTargetFromEvent(element: any): any | undefined { - if (element && !IWorkspaceFolder.isIWorkspaceFolder(element) && !(element as IFileStat).isDirectory) { - return new FileElement((element as IFileStat).resource, FileKind.FILE); + protected _previewElement(_element: any): IDisposable { + return Disposable.None; + } + + async _revealElement(element: IFileStat | IWorkspaceFolder, options: IEditorOptions, sideBySide: boolean): Promise { + let resource: URI | undefined; + if (isWorkspaceFolder(element)) { + resource = element.uri; + } else if (!element.isDirectory) { + resource = element.resource; } + if (resource) { + this._onWillPickElement.fire(); + await this._editorService.openEditor({ resource, options }, sideBySide ? SIDE_GROUP : undefined); + return true; + } + return false; } } //#endregion -//#region - Symbols +//#region - Outline + +class OutlineTreeSorter implements ITreeSorter { + + private _order: 'name' | 'type' | 'position'; + + constructor( + private comparator: IOutlineComparator, + uri: URI | undefined, + @ITextResourceConfigurationService configService: ITextResourceConfigurationService, + ) { + this._order = configService.getValue(uri, 'breadcrumbs.symbolSortOrder'); + } + + compare(a: E, b: E): number { + if (this._order === 'name') { + return this.comparator.compareByName(a, b); + } else if (this._order === 'type') { + return this.comparator.compareByType(a, b); + } else { + return this.comparator.compareByPosition(a, b); + } + } +} export class BreadcrumbsOutlinePicker extends BreadcrumbsPicker { - protected readonly _symbolSortOrder: BreadcrumbsConfig<'position' | 'name' | 'type'>; - protected _outlineComparator: OutlineItemComparator; + protected _createTree(container: HTMLElement, input: OutlineElement2) { - constructor( - parent: HTMLElement, - @IInstantiationService instantiationService: IInstantiationService, - @IThemeService themeService: IThemeService, - @IConfigurationService configurationService: IConfigurationService, - @IModeService private readonly _modeService: IModeService, - ) { - super(parent, instantiationService, themeService, configurationService); - this._symbolSortOrder = BreadcrumbsConfig.SymbolSortOrder.bindTo(this._configurationService); - this._outlineComparator = new OutlineItemComparator(); - } + const { config } = input.outline; - protected _createTree(container: HTMLElement) { - return >this._instantiationService.createInstance( + return , any, FuzzyScore>>this._instantiationService.createInstance( WorkbenchDataTree, 'BreadcrumbsOutlinePicker', container, - new OutlineVirtualDelegate(), - [new OutlineGroupRenderer(), this._instantiationService.createInstance(OutlineElementRenderer)], - new OutlineDataSource(), + config.delegate, + config.renderers, + config.treeDataSource, { + ...config.options, + sorter: this._instantiationService.createInstance(OutlineTreeSorter, config.comparator, undefined), collapseByDefault: true, expandOnlyOnTwistieClick: true, multipleSelectionSupport: false, - sorter: this._outlineComparator, - identityProvider: new OutlineIdentityProvider(), - keyboardNavigationLabelProvider: new OutlineNavigationLabelProvider(), - accessibilityProvider: new OutlineAccessibilityProvider(localize('breadcrumbs', "Breadcrumbs")), - filter: this._instantiationService.createInstance(OutlineFilter, 'breadcrumbs') } ); } - dispose(): void { - this._symbolSortOrder.dispose(); - super.dispose(); - } + protected _setInput(input: OutlineElement2): Promise { - protected _setInput(input: BreadcrumbElement): Promise { - const element = input as TreeElement; - const model = OutlineModel.get(element)!; - const tree = this._tree as WorkbenchDataTree; + const viewState = input.outline.captureViewState(); + this.restoreViewState = () => { viewState.dispose(); }; - const overrideConfiguration = { - resource: model.uri, - overrideIdentifier: this._modeService.getModeIdByFilepathOrFirstLine(model.uri) - }; - this._outlineComparator.type = this._getOutlineItemCompareType(overrideConfiguration); + const tree = this._tree as WorkbenchDataTree, any, FuzzyScore>; - tree.setInput(model); - if (element !== model) { - tree.reveal(element, 0.5); - tree.setFocus([element], this._fakeEvent); + tree.setInput(input.outline); + if (input.element !== input.outline) { + tree.reveal(input.element, 0.5); + tree.setFocus([input.element], this._fakeEvent); } tree.domFocus(); return Promise.resolve(); } - protected _getTargetFromEvent(element: any): any | undefined { - if (element instanceof OutlineElement) { - return element; - } + protected _previewElement(element: any): IDisposable { + const outline: IOutline = this._tree.getInput(); + return outline.preview(element); } - private _getOutlineItemCompareType(overrideConfiguration?: IConfigurationOverrides): OutlineSortOrder { - switch (this._symbolSortOrder.getValue(overrideConfiguration)) { - case 'name': - return OutlineSortOrder.ByName; - case 'type': - return OutlineSortOrder.ByKind; - case 'position': - default: - return OutlineSortOrder.ByPosition; - } + async _revealElement(element: any, options: IEditorOptions, sideBySide: boolean): Promise { + this._onWillPickElement.fire(); + const outline: IOutline = this._tree.getInput(); + await outline.reveal(element, options, sideBySide); + return true; } } diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index fdb0a57a..5fdcc208 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -33,7 +33,7 @@ import { JoinAllGroupsAction, FocusLeftGroup, FocusAboveGroup, FocusRightGroup, FocusBelowGroup, EditorLayoutSingleAction, EditorLayoutTwoColumnsAction, EditorLayoutThreeColumnsAction, EditorLayoutTwoByTwoGridAction, EditorLayoutTwoRowsAction, EditorLayoutThreeRowsAction, EditorLayoutTwoColumnsBottomAction, EditorLayoutTwoRowsRightAction, NewEditorGroupLeftAction, NewEditorGroupRightAction, NewEditorGroupAboveAction, NewEditorGroupBelowAction, SplitEditorOrthogonalAction, CloseEditorInAllGroupsAction, NavigateToLastEditLocationAction, ToggleGroupSizesAction, ShowAllEditorsByMostRecentlyUsedAction, - QuickAccessPreviousRecentlyUsedEditorAction, OpenPreviousRecentlyUsedEditorInGroupAction, OpenNextRecentlyUsedEditorInGroupAction, QuickAccessLeastRecentlyUsedEditorAction, QuickAccessLeastRecentlyUsedEditorInGroupAction, ReopenResourcesAction, ToggleEditorTypeAction + QuickAccessPreviousRecentlyUsedEditorAction, OpenPreviousRecentlyUsedEditorInGroupAction, OpenNextRecentlyUsedEditorInGroupAction, QuickAccessLeastRecentlyUsedEditorAction, QuickAccessLeastRecentlyUsedEditorInGroupAction, ReopenResourcesAction, ToggleEditorTypeAction, DuplicateGroupDownAction, DuplicateGroupLeftAction, DuplicateGroupRightAction, DuplicateGroupUpAction } from 'vs/workbench/browser/parts/editor/editorActions'; import * as editorCommands from 'vs/workbench/browser/parts/editor/editorCommands'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -42,7 +42,7 @@ import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/co import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { isMacintosh } from 'vs/base/common/platform'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { OpenWorkspaceButtonContribution } from 'vs/workbench/browser/parts/editor/editorWidgets'; +import { OpenWorkspaceButtonContribution } from 'vs/workbench/browser/codeeditor'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { toLocalResource } from 'vs/base/common/resources'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; @@ -351,6 +351,10 @@ registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveGroupLeftAction, registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveGroupRightAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.RightArrow) }), 'View: Move Editor Group Right', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveGroupUpAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.UpArrow) }), 'View: Move Editor Group Up', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveGroupDownAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.DownArrow) }), 'View: Move Editor Group Down', CATEGORIES.View.value); +registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateGroupLeftAction), 'View: Duplicate Editor Group Left', CATEGORIES.View.value); +registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateGroupRightAction), 'View: Duplicate Editor Group Right', CATEGORIES.View.value); +registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateGroupUpAction), 'View: Duplicate Editor Group Up', CATEGORIES.View.value); +registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateGroupDownAction), 'View: Duplicate Editor Group Down', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveEditorToPreviousGroupAction, { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.LeftArrow, mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.LeftArrow } }), 'View: Move Editor into Previous Group', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveEditorToNextGroupAction, { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.RightArrow, mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.RightArrow } }), 'View: Move Editor into Next Group', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveEditorToFirstGroupAction, { primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_1, mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_1 } }), 'View: Move Editor into First Group', CATEGORIES.View.value); diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 8b18d4e9..7e901d0a 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -23,6 +23,7 @@ import { AllEditorsByMostRecentlyUsedQuickAccess, ActiveGroupEditorsByMostRecent import { Codicon } from 'vs/base/common/codicons'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { openEditorWith, getAllAvailableEditors } from 'vs/workbench/services/editor/common/editorOpenWith'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export class ExecuteCommandAction extends Action { @@ -746,12 +747,13 @@ export class CloseEditorInAllGroupsAction extends Action { } } -export class BaseMoveGroupAction extends Action { +class BaseMoveCopyGroupAction extends Action { constructor( id: string, label: string, private direction: GroupDirection, + private isMove: boolean, private editorGroupService: IEditorGroupsService ) { super(id, label); @@ -766,9 +768,18 @@ export class BaseMoveGroupAction extends Action { } if (sourceGroup) { - const targetGroup = this.findTargetGroup(sourceGroup); - if (targetGroup) { - this.editorGroupService.moveGroup(sourceGroup, targetGroup, this.direction); + let resultGroup: IEditorGroup | undefined = undefined; + if (this.isMove) { + const targetGroup = this.findTargetGroup(sourceGroup); + if (targetGroup) { + resultGroup = this.editorGroupService.moveGroup(sourceGroup, targetGroup, this.direction); + } + } else { + resultGroup = this.editorGroupService.copyGroup(sourceGroup, sourceGroup, this.direction); + } + + if (resultGroup) { + this.editorGroupService.activateGroup(resultGroup); } } } @@ -801,6 +812,18 @@ export class BaseMoveGroupAction extends Action { } } +class BaseMoveGroupAction extends BaseMoveCopyGroupAction { + + constructor( + id: string, + label: string, + direction: GroupDirection, + editorGroupService: IEditorGroupsService + ) { + super(id, label, direction, true, editorGroupService); + } +} + export class MoveGroupLeftAction extends BaseMoveGroupAction { static readonly ID = 'workbench.action.moveActiveEditorGroupLeft'; @@ -857,6 +880,74 @@ export class MoveGroupDownAction extends BaseMoveGroupAction { } } +class BaseDuplicateGroupAction extends BaseMoveCopyGroupAction { + + constructor( + id: string, + label: string, + direction: GroupDirection, + editorGroupService: IEditorGroupsService + ) { + super(id, label, direction, false, editorGroupService); + } +} + +export class DuplicateGroupLeftAction extends BaseDuplicateGroupAction { + + static readonly ID = 'workbench.action.duplicateActiveEditorGroupLeft'; + static readonly LABEL = nls.localize('duplicateActiveGroupLeft', "Duplicate Editor Group Left"); + + constructor( + id: string, + label: string, + @IEditorGroupsService editorGroupService: IEditorGroupsService + ) { + super(id, label, GroupDirection.LEFT, editorGroupService); + } +} + +export class DuplicateGroupRightAction extends BaseDuplicateGroupAction { + + static readonly ID = 'workbench.action.duplicateActiveEditorGroupRight'; + static readonly LABEL = nls.localize('duplicateActiveGroupRight', "Duplicate Editor Group Right"); + + constructor( + id: string, + label: string, + @IEditorGroupsService editorGroupService: IEditorGroupsService + ) { + super(id, label, GroupDirection.RIGHT, editorGroupService); + } +} + +export class DuplicateGroupUpAction extends BaseDuplicateGroupAction { + + static readonly ID = 'workbench.action.duplicateActiveEditorGroupUp'; + static readonly LABEL = nls.localize('duplicateActiveGroupUp', "Duplicate Editor Group Up"); + + constructor( + id: string, + label: string, + @IEditorGroupsService editorGroupService: IEditorGroupsService + ) { + super(id, label, GroupDirection.UP, editorGroupService); + } +} + +export class DuplicateGroupDownAction extends BaseDuplicateGroupAction { + + static readonly ID = 'workbench.action.duplicateActiveEditorGroupDown'; + static readonly LABEL = nls.localize('duplicateActiveGroupDown', "Duplicate Editor Group Down"); + + constructor( + id: string, + label: string, + @IEditorGroupsService editorGroupService: IEditorGroupsService + ) { + super(id, label, GroupDirection.DOWN, editorGroupService); + } +} + export class MinimizeOtherGroupsAction extends Action { static readonly ID = 'workbench.action.minimizeOtherEditors'; @@ -1803,9 +1894,8 @@ export class ReopenResourcesAction extends Action { constructor( id: string, label: string, - @IQuickInputService private readonly quickInputService: IQuickInputService, @IEditorService private readonly editorService: IEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(id, label); } @@ -1823,7 +1913,7 @@ export class ReopenResourcesAction extends Action { const options = activeEditorPane.options; const group = activeEditorPane.group; - await openEditorWith(activeInput, undefined, options, group, this.editorService, this.configurationService, this.quickInputService); + await this.instantiationService.invokeFunction(openEditorWith, activeInput, undefined, options, group); } } diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index dcece534..f9fd2d57 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -23,7 +23,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { ActiveGroupEditorsByMostRecentlyUsedQuickAccess } from 'vs/workbench/browser/parts/editor/editorQuickAccess'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { openEditorWith } from 'vs/workbench/services/editor/common/editorOpenWith'; @@ -484,7 +483,6 @@ function registerOpenEditorAPICommands(): void { const editorService = accessor.get(IEditorService); const editorGroupsService = accessor.get(IEditorGroupsService); const configurationService = accessor.get(IConfigurationService); - const quickInputService = accessor.get(IQuickInputService); const [columnArg, optionsArg] = columnAndOptions ?? []; let group: IEditorGroup | undefined = undefined; @@ -504,7 +502,7 @@ function registerOpenEditorAPICommands(): void { const textOptions: ITextEditorOptions = optionsArg ? { ...optionsArg, override: false } : { override: false }; const input = editorService.createEditorInput({ resource: URI.revive(resource) }); - return openEditorWith(input, id, textOptions, group, editorService, configurationService, quickInputService); + return openEditorWith(accessor, input, id, textOptions, group); }); } @@ -902,24 +900,10 @@ function registerOtherEditorCommands(): void { id: TOGGLE_KEEP_EDITORS_COMMAND_ID, handler: accessor => { const configurationService = accessor.get(IConfigurationService); - const notificationService = accessor.get(INotificationService); - const openerService = accessor.get(IOpenerService); - // Update setting const currentSetting = configurationService.getValue('workbench.editor.enablePreview'); const newSetting = currentSetting === true ? false : true; configurationService.updateValue('workbench.editor.enablePreview', newSetting); - - // Inform user - notificationService.prompt( - Severity.Info, - newSetting ? - nls.localize('enablePreview', "Preview editors have been enabled in settings.") : - nls.localize('disablePreview', "Preview editors have been disabled in settings."), - [{ - label: nls.localize('learnMode', "Learn More"), run: () => openerService.open('https://go.microsoft.com/fwlink/?linkid=2147473') - }] - ); } }); diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 50f57156..d3d2258f 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -283,7 +283,8 @@ class DropOverlay extends Themable { // Open in target group const options = getActiveTextEditorOptions(sourceGroup, draggedEditor.editor, EditorOptions.create({ pinned: true, // always pin dropped editor - sticky: sourceGroup.isSticky(draggedEditor.editor) // preserve sticky state + sticky: sourceGroup.isSticky(draggedEditor.editor), // preserve sticky state + override: false, // Use `draggedEditor.editor` as is. If it is already a custom editor, it will stay so. })); const copyEditor = this.isCopyOperation(event, draggedEditor); targetGroup.openEditor(draggedEditor.editor, options, copyEditor ? OpenEditorContext.COPY_EDITOR : OpenEditorContext.MOVE_EDITOR); diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index a212456d..277f029c 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -322,8 +322,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private createContainerContextMenu(): void { const menu = this._register(this.menuService.createMenu(MenuId.EmptyEditorGroupContext, this.contextKeyService)); - this._register(addDisposableListener(this.element, EventType.CONTEXT_MENU, event => this.onShowContainerContextMenu(menu, event))); - this._register(addDisposableListener(this.element, TouchEventType.Contextmenu, event => this.onShowContainerContextMenu(menu))); + this._register(addDisposableListener(this.element, EventType.CONTEXT_MENU, e => this.onShowContainerContextMenu(menu, e))); + this._register(addDisposableListener(this.element, TouchEventType.Contextmenu, () => this.onShowContainerContextMenu(menu))); } private onShowContainerContextMenu(menu: IMenu, e?: MouseEvent): void { @@ -1704,13 +1704,15 @@ export class EditorGroupView extends Themable implements IEditorGroupView { layout(width: number, height: number): void { this.dimension = new Dimension(width, height); - // Ensure editor container gets height as CSS depending on the preferred height of the title control - const titleHeight = this.titleDimensions.height; - const editorHeight = Math.max(0, height - titleHeight); - this.editorContainer.style.height = `${editorHeight}px`; + // Layout the title area first to receive the size it occupies + const titleAreaSize = this.titleAreaControl.layout({ + container: this.dimension, + available: new Dimension(width, height - this.editorControl.minimumHeight) + }); - // Forward to controls - this.titleAreaControl.layout(new Dimension(width, titleHeight)); + // Pass the container width and remaining height to the editor layout + const editorHeight = Math.max(0, height - titleAreaSize.height); + this.editorContainer.style.height = `${editorHeight}px`; this.editorControl.layout(new Dimension(width, editorHeight)); } @@ -1769,7 +1771,7 @@ export interface EditorReplacement { readonly options?: EditorOptions; } -registerThemingParticipant((theme, collector, environment) => { +registerThemingParticipant((theme, collector) => { // Letterpress const letterpress = `./media/letterpress${theme.type === 'dark' ? '-dark' : theme.type === 'hc' ? '-hc' : ''}.svg`; diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 1d85f0ad..3d289709 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/editorstatus'; -import * as nls from 'vs/nls'; +import { localize } from 'vs/nls'; import { runAtThisOrScheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; import { format, compare, splitLines } from 'vs/base/common/strings'; import { extname, basename, isEqual } from 'vs/base/common/resources'; @@ -283,14 +283,15 @@ class State { } } -const nlsSingleSelectionRange = nls.localize('singleSelectionRange', "Ln {0}, Col {1} ({2} selected)"); -const nlsSingleSelection = nls.localize('singleSelection', "Ln {0}, Col {1}"); -const nlsMultiSelectionRange = nls.localize('multiSelectionRange', "{0} selections ({1} characters selected)"); -const nlsMultiSelection = nls.localize('multiSelection', "{0} selections"); -const nlsEOLLF = nls.localize('endOfLineLineFeed', "LF"); -const nlsEOLCRLF = nls.localize('endOfLineCarriageReturnLineFeed', "CRLF"); +const nlsSingleSelectionRange = localize('singleSelectionRange', "Ln {0}, Col {1} ({2} selected)"); +const nlsSingleSelection = localize('singleSelection', "Ln {0}, Col {1}"); +const nlsMultiSelectionRange = localize('multiSelectionRange', "{0} selections ({1} characters selected)"); +const nlsMultiSelection = localize('multiSelection', "{0} selections"); +const nlsEOLLF = localize('endOfLineLineFeed', "LF"); +const nlsEOLCRLF = localize('endOfLineCarriageReturnLineFeed', "CRLF"); export class EditorStatus extends Disposable implements IWorkbenchContribution { + private readonly tabFocusModeElement = this._register(new MutableDisposable()); private readonly columnSelectionModeElement = this._register(new MutableDisposable()); private readonly screenRedearModeElement = this._register(new MutableDisposable()); @@ -342,14 +343,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { if (!this.screenReaderNotification) { this.screenReaderNotification = this.notificationService.prompt( Severity.Info, - nls.localize('screenReaderDetectedExplanation.question', "Are you using a screen reader to operate VS Code? (word wrap is disabled when using a screen reader)"), + localize('screenReaderDetectedExplanation.question', "Are you using a screen reader to operate VS Code? (word wrap is disabled when using a screen reader)"), [{ - label: nls.localize('screenReaderDetectedExplanation.answerYes', "Yes"), + label: localize('screenReaderDetectedExplanation.answerYes', "Yes"), run: () => { this.configurationService.updateValue('editor.accessibilitySupport', 'on'); } }, { - label: nls.localize('screenReaderDetectedExplanation.answerNo', "No"), + label: localize('screenReaderDetectedExplanation.answerNo', "No"), run: () => { this.configurationService.updateValue('editor.accessibilitySupport', 'off'); } @@ -364,11 +365,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { private async showIndentationPicker(): Promise { const activeTextEditorControl = getCodeEditor(this.editorService.activeTextEditorControl); if (!activeTextEditorControl) { - return this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + return this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]); } if (this.editorService.activeEditor?.isReadonly()) { - return this.quickInputService.pick([{ label: nls.localize('noWritableCodeEditor', "The active code editor is read-only.") }]); + return this.quickInputService.pick([{ label: localize('noWritableCodeEditor', "The active code editor is read-only.") }]); } const picks: QuickPickInput[] = [ @@ -390,25 +391,25 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { }; }); - picks.splice(3, 0, { type: 'separator', label: nls.localize('indentConvert', "convert file") }); - picks.unshift({ type: 'separator', label: nls.localize('indentView', "change view") }); + picks.splice(3, 0, { type: 'separator', label: localize('indentConvert', "convert file") }); + picks.unshift({ type: 'separator', label: localize('indentView', "change view") }); - const action = await this.quickInputService.pick(picks, { placeHolder: nls.localize('pickAction', "Select Action"), matchOnDetail: true }); + const action = await this.quickInputService.pick(picks, { placeHolder: localize('pickAction', "Select Action"), matchOnDetail: true }); return action?.run(); } private updateTabFocusModeElement(visible: boolean): void { if (visible) { if (!this.tabFocusModeElement.value) { - const text = nls.localize('tabFocusModeEnabled', "Tab Moves Focus"); + const text = localize('tabFocusModeEnabled', "Tab Moves Focus"); this.tabFocusModeElement.value = this.statusbarService.addEntry({ text, ariaLabel: text, - tooltip: nls.localize('disableTabMode', "Disable Accessibility Mode"), + tooltip: localize('disableTabMode', "Disable Accessibility Mode"), command: 'editor.action.toggleTabFocusMode', backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) - }, 'status.editor.tabFocusMode', nls.localize('status.editor.tabFocusMode', "Accessibility Mode"), StatusbarAlignment.RIGHT, 100.7); + }, 'status.editor.tabFocusMode', localize('status.editor.tabFocusMode', "Accessibility Mode"), StatusbarAlignment.RIGHT, 100.7); } } else { this.tabFocusModeElement.clear(); @@ -418,15 +419,15 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { private updateColumnSelectionModeElement(visible: boolean): void { if (visible) { if (!this.columnSelectionModeElement.value) { - const text = nls.localize('columnSelectionModeEnabled', "Column Selection"); + const text = localize('columnSelectionModeEnabled', "Column Selection"); this.columnSelectionModeElement.value = this.statusbarService.addEntry({ text, ariaLabel: text, - tooltip: nls.localize('disableColumnSelectionMode', "Disable Column Selection Mode"), + tooltip: localize('disableColumnSelectionMode', "Disable Column Selection Mode"), command: 'editor.action.toggleColumnSelection', backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) - }, 'status.editor.columnSelectionMode', nls.localize('status.editor.columnSelectionMode', "Column Selection Mode"), StatusbarAlignment.RIGHT, 100.8); + }, 'status.editor.columnSelectionMode', localize('status.editor.columnSelectionMode', "Column Selection Mode"), StatusbarAlignment.RIGHT, 100.8); } } else { this.columnSelectionModeElement.clear(); @@ -436,14 +437,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { private updateScreenReaderModeElement(visible: boolean): void { if (visible) { if (!this.screenRedearModeElement.value) { - const text = nls.localize('screenReaderDetected', "Screen Reader Optimized"); + const text = localize('screenReaderDetected', "Screen Reader Optimized"); this.screenRedearModeElement.value = this.statusbarService.addEntry({ text, ariaLabel: text, command: 'showEditorScreenReaderNotification', backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) - }, 'status.editor.screenReaderMode', nls.localize('status.editor.screenReaderMode', "Screen Reader Mode"), StatusbarAlignment.RIGHT, 100.6); + }, 'status.editor.screenReaderMode', localize('status.editor.screenReaderMode', "Screen Reader Mode"), StatusbarAlignment.RIGHT, 100.6); } } else { this.screenRedearModeElement.clear(); @@ -459,11 +460,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const props: IStatusbarEntry = { text, ariaLabel: text, - tooltip: nls.localize('gotoLine', "Go to Line/Column"), + tooltip: localize('gotoLine', "Go to Line/Column"), command: 'workbench.action.gotoLine' }; - this.updateElement(this.selectionElement, props, 'status.editor.selection', nls.localize('status.editor.selection', "Editor Selection"), StatusbarAlignment.RIGHT, 100.5); + this.updateElement(this.selectionElement, props, 'status.editor.selection', localize('status.editor.selection', "Editor Selection"), StatusbarAlignment.RIGHT, 100.5); } private updateIndentationElement(text: string | undefined): void { @@ -475,11 +476,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const props: IStatusbarEntry = { text, ariaLabel: text, - tooltip: nls.localize('selectIndentation', "Select Indentation"), + tooltip: localize('selectIndentation', "Select Indentation"), command: 'changeEditorIndentation' }; - this.updateElement(this.indentationElement, props, 'status.editor.indentation', nls.localize('status.editor.indentation', "Editor Indentation"), StatusbarAlignment.RIGHT, 100.4); + this.updateElement(this.indentationElement, props, 'status.editor.indentation', localize('status.editor.indentation', "Editor Indentation"), StatusbarAlignment.RIGHT, 100.4); } private updateEncodingElement(text: string | undefined): void { @@ -491,11 +492,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const props: IStatusbarEntry = { text, ariaLabel: text, - tooltip: nls.localize('selectEncoding', "Select Encoding"), + tooltip: localize('selectEncoding', "Select Encoding"), command: 'workbench.action.editor.changeEncoding' }; - this.updateElement(this.encodingElement, props, 'status.editor.encoding', nls.localize('status.editor.encoding', "Editor Encoding"), StatusbarAlignment.RIGHT, 100.3); + this.updateElement(this.encodingElement, props, 'status.editor.encoding', localize('status.editor.encoding', "Editor Encoding"), StatusbarAlignment.RIGHT, 100.3); } private updateEOLElement(text: string | undefined): void { @@ -507,11 +508,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const props: IStatusbarEntry = { text, ariaLabel: text, - tooltip: nls.localize('selectEOL', "Select End of Line Sequence"), + tooltip: localize('selectEOL', "Select End of Line Sequence"), command: 'workbench.action.editor.changeEOL' }; - this.updateElement(this.eolElement, props, 'status.editor.eol', nls.localize('status.editor.eol', "Editor End of Line"), StatusbarAlignment.RIGHT, 100.2); + this.updateElement(this.eolElement, props, 'status.editor.eol', localize('status.editor.eol', "Editor End of Line"), StatusbarAlignment.RIGHT, 100.2); } private updateModeElement(text: string | undefined): void { @@ -523,11 +524,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const props: IStatusbarEntry = { text, ariaLabel: text, - tooltip: nls.localize('selectLanguageMode', "Select Language Mode"), + tooltip: localize('selectLanguageMode', "Select Language Mode"), command: 'workbench.action.editor.changeLanguageMode' }; - this.updateElement(this.modeElement, props, 'status.editor.mode', nls.localize('status.editor.mode', "Editor Language"), StatusbarAlignment.RIGHT, 100.1); + this.updateElement(this.modeElement, props, 'status.editor.mode', localize('status.editor.mode', "Editor Language"), StatusbarAlignment.RIGHT, 100.1); } private updateMetadataElement(text: string | undefined): void { @@ -539,10 +540,10 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const props: IStatusbarEntry = { text, ariaLabel: text, - tooltip: nls.localize('fileInfo', "File Information") + tooltip: localize('fileInfo', "File Information") }; - this.updateElement(this.metadataElement, props, 'status.editor.info', nls.localize('status.editor.info', "File Information"), StatusbarAlignment.RIGHT, 100); + this.updateElement(this.metadataElement, props, 'status.editor.info', localize('status.editor.info', "File Information"), StatusbarAlignment.RIGHT, 100); } private updateElement(element: MutableDisposable, props: IStatusbarEntry, id: string, name: string, alignment: StatusbarAlignment, priority: number) { @@ -730,8 +731,8 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const modelOpts = model.getOptions(); update.indentation = ( modelOpts.insertSpaces - ? nls.localize('spacesSize', "Spaces: {0}", modelOpts.indentSize) - : nls.localize({ key: 'tabSize', comment: ['Tab corresponds to the tab key'] }, "Tab Size: {0}", modelOpts.tabSize) + ? localize('spacesSize', "Spaces: {0}", modelOpts.indentSize) + : localize({ key: 'tabSize', comment: ['Tab corresponds to the tab key'] }, "Tab Size: {0}", modelOpts.tabSize) ); } } @@ -766,7 +767,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { if (editorWidget) { const screenReaderDetected = this.accessibilityService.isScreenReaderOptimized(); if (screenReaderDetected) { - const screenReaderConfiguration = this.configurationService.getValue('editor').accessibilitySupport; + const screenReaderConfiguration = this.configurationService.getValue('editor')?.accessibilitySupport; if (screenReaderConfiguration === 'auto') { if (!this.promptedScreenReader) { this.promptedScreenReader = true; @@ -921,7 +922,7 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { const line = splitLines(this.currentMarker.message)[0]; const text = `${this.getType(this.currentMarker)} ${line}`; if (!this.statusBarEntryAccessor.value) { - this.statusBarEntryAccessor.value = this.statusbarService.addEntry({ text: '', ariaLabel: '' }, 'statusbar.currentProblem', nls.localize('currentProblem', "Current Problem"), StatusbarAlignment.LEFT); + this.statusBarEntryAccessor.value = this.statusbarService.addEntry({ text: '', ariaLabel: '' }, 'statusbar.currentProblem', localize('currentProblem', "Current Problem"), StatusbarAlignment.LEFT); } this.statusBarEntryAccessor.value.update({ text, ariaLabel: text }); } else { @@ -934,9 +935,11 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { if (!currentMarker) { return true; } + if (!previousMarker) { return true; } + return IMarkerData.makeKey(previousMarker) !== IMarkerData.makeKey(currentMarker); } @@ -946,6 +949,7 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { case MarkerSeverity.Warning: return '$(warning)'; case MarkerSeverity.Info: return '$(info)'; } + return ''; } @@ -953,17 +957,21 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { if (!this.configurationService.getValue('problems.showCurrentInStatus')) { return null; } + if (!this.editor) { return null; } + const model = this.editor.getModel(); if (!model) { return null; } + const position = this.editor.getPosition(); if (!position) { return null; } + return this.markers.find(marker => Range.containsPosition(marker, position)) || null; } @@ -971,13 +979,16 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { if (!this.editor) { return; } + const model = this.editor.getModel(); if (!model) { return; } + if (model && !changedResources.some(r => isEqual(model.uri, r))) { return; } + this.updateMarkers(); } @@ -985,10 +996,12 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { if (!this.editor) { return; } + const model = this.editor.getModel(); if (!model) { return; } + if (model) { this.markers = this.markerService.read({ resource: model.uri, @@ -998,6 +1011,7 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { } else { this.markers = []; } + this.updateStatus(); } } @@ -1007,9 +1021,11 @@ function compareMarker(a: IMarker, b: IMarker): number { if (res === 0) { res = MarkerSeverity.compare(a.severity, b.severity); } + if (res === 0) { res = Range.compareRangesUsingStarts(a, b); } + return res; } @@ -1022,7 +1038,7 @@ export class ShowLanguageExtensionsAction extends Action { @ICommandService private readonly commandService: ICommandService, @IExtensionGalleryService galleryService: IExtensionGalleryService ) { - super(ShowLanguageExtensionsAction.ID, nls.localize('showLanguageExtensions', "Search Marketplace Extensions for '{0}'...", fileExtension)); + super(ShowLanguageExtensionsAction.ID, localize('showLanguageExtensions', "Search Marketplace Extensions for '{0}'...", fileExtension)); this.enabled = galleryService.isEnabled(); } @@ -1035,7 +1051,7 @@ export class ShowLanguageExtensionsAction extends Action { export class ChangeModeAction extends Action { static readonly ID = 'workbench.action.editor.changeLanguageMode'; - static readonly LABEL = nls.localize('changeMode', "Change Language Mode"); + static readonly LABEL = localize('changeMode', "Change Language Mode"); constructor( actionId: string, @@ -1054,14 +1070,14 @@ export class ChangeModeAction extends Action { async run(): Promise { const activeEditorPane = this.editorService.activeEditorPane as unknown as { isNotebookEditor?: boolean } | undefined; - if (activeEditorPane?.isNotebookEditor) { + if (activeEditorPane?.isNotebookEditor) { // TODO@rebornix TODO@jrieken debt: https://github.com/microsoft/vscode/issues/114554 // it's inside notebook editor return this.commandService.executeCommand('notebook.cell.changeLanguage'); } const activeTextEditorControl = getCodeEditor(this.editorService.activeTextEditorControl); if (!activeTextEditorControl) { - await this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + await this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]); return; } @@ -1085,22 +1101,24 @@ export class ChangeModeAction extends Action { const languages = this.modeService.getRegisteredLanguageNames(); const picks: QuickPickInput[] = languages.sort().map((lang, index) => { const modeId = this.modeService.getModeIdForLanguageName(lang.toLowerCase()) || 'unknown'; + const extensions = this.modeService.getExtensions(lang).join(' '); let description: string; if (currentLanguageId === lang) { - description = nls.localize('languageDescription', "({0}) - Configured Language", modeId); + description = localize('languageDescription', "({0}) - Configured Language", modeId); } else { - description = nls.localize('languageDescriptionConfigured', "({0})", modeId); + description = localize('languageDescriptionConfigured', "({0})", modeId); } return { label: lang, + meta: extensions, iconClasses: getIconClassesForModeId(modeId), description }; }); if (hasLanguageSupport) { - picks.unshift({ type: 'separator', label: nls.localize('languagesPicks', "languages (identifier)") }); + picks.unshift({ type: 'separator', label: localize('languagesPicks', "languages (identifier)") }); } // Offer action to configure via settings @@ -1115,22 +1133,22 @@ export class ChangeModeAction extends Action { picks.unshift(galleryAction); } - configureModeSettings = { label: nls.localize('configureModeSettings', "Configure '{0}' language based settings...", currentLanguageId) }; + configureModeSettings = { label: localize('configureModeSettings', "Configure '{0}' language based settings...", currentLanguageId) }; picks.unshift(configureModeSettings); - configureModeAssociations = { label: nls.localize('configureAssociationsExt', "Configure File Association for '{0}'...", ext) }; + configureModeAssociations = { label: localize('configureAssociationsExt', "Configure File Association for '{0}'...", ext) }; picks.unshift(configureModeAssociations); } // Offer to "Auto Detect" const autoDetectMode: IQuickPickItem = { - label: nls.localize('autoDetect', "Auto Detect") + label: localize('autoDetect', "Auto Detect") }; if (hasLanguageSupport) { picks.unshift(autoDetectMode); } - const pick = await this.quickInputService.pick(picks, { placeHolder: nls.localize('pickLanguage', "Select Language Mode"), matchOnDescription: true }); + const pick = await this.quickInputService.pick(picks, { placeHolder: localize('pickLanguage', "Select Language Mode"), matchOnDescription: true }); if (!pick) { return; } @@ -1178,6 +1196,8 @@ export class ChangeModeAction extends Action { modeSupport.setMode(languageSelection.languageIdentifier.language); } } + + activeTextEditorControl.focus(); } } @@ -1194,12 +1214,12 @@ export class ChangeModeAction extends Action { id, label: lang, iconClasses: getIconClassesForModeId(id), - description: (id === currentAssociation) ? nls.localize('currentAssociation', "Current Association") : undefined + description: (id === currentAssociation) ? localize('currentAssociation', "Current Association") : undefined }; }); setTimeout(async () => { - const language = await this.quickInputService.pick(picks, { placeHolder: nls.localize('pickLanguageToConfigure', "Select Language Mode to Associate with '{0}'", extension || base) }); + const language = await this.quickInputService.pick(picks, { placeHolder: localize('pickLanguageToConfigure', "Select Language Mode to Associate with '{0}'", extension || base) }); if (language) { const fileAssociationsConfig = this.configurationService.inspect<{}>(FILES_ASSOCIATIONS_CONFIG); @@ -1233,7 +1253,7 @@ export interface IChangeEOLEntry extends IQuickPickItem { export class ChangeEOLAction extends Action { static readonly ID = 'workbench.action.editor.changeEOL'; - static readonly LABEL = nls.localize('changeEndOfLine', "Change End of Line Sequence"); + static readonly LABEL = localize('changeEndOfLine', "Change End of Line Sequence"); constructor( actionId: string, @@ -1247,12 +1267,12 @@ export class ChangeEOLAction extends Action { async run(): Promise { const activeTextEditorControl = getCodeEditor(this.editorService.activeTextEditorControl); if (!activeTextEditorControl) { - await this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + await this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]); return; } if (this.editorService.activeEditor?.isReadonly()) { - await this.quickInputService.pick([{ label: nls.localize('noWritableCodeEditor', "The active code editor is read-only.") }]); + await this.quickInputService.pick([{ label: localize('noWritableCodeEditor', "The active code editor is read-only.") }]); return; } @@ -1265,7 +1285,7 @@ export class ChangeEOLAction extends Action { const selectedIndex = (textModel?.getEOL() === '\n') ? 0 : 1; - const eol = await this.quickInputService.pick(EOLOptions, { placeHolder: nls.localize('pickEndOfLine', "Select End of Line Sequence"), activeItem: EOLOptions[selectedIndex] }); + const eol = await this.quickInputService.pick(EOLOptions, { placeHolder: localize('pickEndOfLine', "Select End of Line Sequence"), activeItem: EOLOptions[selectedIndex] }); if (eol) { const activeCodeEditor = getCodeEditor(this.editorService.activeTextEditorControl); if (activeCodeEditor?.hasModel() && !this.editorService.activeEditor?.isReadonly()) { @@ -1275,13 +1295,15 @@ export class ChangeEOLAction extends Action { textModel.pushStackElement(); } } + + activeTextEditorControl.focus(); } } export class ChangeEncodingAction extends Action { static readonly ID = 'workbench.action.editor.changeEncoding'; - static readonly LABEL = nls.localize('changeEncoding', "Change File Encoding"); + static readonly LABEL = localize('changeEncoding', "Change File Encoding"); constructor( actionId: string, @@ -1296,25 +1318,26 @@ export class ChangeEncodingAction extends Action { } async run(): Promise { - if (!getCodeEditor(this.editorService.activeTextEditorControl)) { - await this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + const activeTextEditorControl = getCodeEditor(this.editorService.activeTextEditorControl); + if (!activeTextEditorControl) { + await this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]); return; } const activeEditorPane = this.editorService.activeEditorPane; if (!activeEditorPane) { - await this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + await this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]); return; } const encodingSupport: IEncodingSupport | null = toEditorWithEncodingSupport(activeEditorPane.input); if (!encodingSupport) { - await this.quickInputService.pick([{ label: nls.localize('noFileEditor', "No file active at this time") }]); + await this.quickInputService.pick([{ label: localize('noFileEditor', "No file active at this time") }]); return; } - const saveWithEncodingPick: IQuickPickItem = { label: nls.localize('saveWithEncoding', "Save with Encoding") }; - const reopenWithEncodingPick: IQuickPickItem = { label: nls.localize('reopenWithEncoding', "Reopen with Encoding") }; + const saveWithEncodingPick: IQuickPickItem = { label: localize('saveWithEncoding', "Save with Encoding") }; + const reopenWithEncodingPick: IQuickPickItem = { label: localize('reopenWithEncoding', "Reopen with Encoding") }; if (!Language.isDefaultVariant()) { const saveWithEncodingAlias = 'Save with Encoding'; @@ -1334,7 +1357,7 @@ export class ChangeEncodingAction extends Action { } else if (activeEditorPane.input.isReadonly()) { action = reopenWithEncodingPick; } else { - action = await this.quickInputService.pick([reopenWithEncodingPick, saveWithEncodingPick], { placeHolder: nls.localize('pickAction', "Select Action"), matchOnDetail: true }); + action = await this.quickInputService.pick([reopenWithEncodingPick, saveWithEncodingPick], { placeHolder: localize('pickAction', "Select Action"), matchOnDetail: true }); } if (!action) { @@ -1394,11 +1417,11 @@ export class ChangeEncodingAction extends Action { // If we have a guessed encoding, show it first unless it matches the configured encoding if (guessedEncoding && configuredEncoding !== guessedEncoding && SUPPORTED_ENCODINGS[guessedEncoding]) { picks.unshift({ type: 'separator' }); - picks.unshift({ id: guessedEncoding, label: SUPPORTED_ENCODINGS[guessedEncoding].labelLong, description: nls.localize('guessedEncoding', "Guessed from content") }); + picks.unshift({ id: guessedEncoding, label: SUPPORTED_ENCODINGS[guessedEncoding].labelLong, description: localize('guessedEncoding', "Guessed from content") }); } const encoding = await this.quickInputService.pick(picks, { - placeHolder: isReopenWithEncoding ? nls.localize('pickEncodingForReopen', "Select File Encoding to Reopen File") : nls.localize('pickEncodingForSave', "Select File Encoding to Save with"), + placeHolder: isReopenWithEncoding ? localize('pickEncodingForReopen', "Select File Encoding to Reopen File") : localize('pickEncodingForSave', "Select File Encoding to Save with"), activeItem: items[typeof directMatchIndex === 'number' ? directMatchIndex : typeof aliasMatchIndex === 'number' ? aliasMatchIndex : -1] }); @@ -1414,5 +1437,7 @@ export class ChangeEncodingAction extends Action { if (typeof encoding.id !== 'undefined' && activeEncodingSupport && activeEncodingSupport.getEncoding() !== encoding.id) { activeEncodingSupport.setEncoding(encoding.id, isReopenWithEncoding ? EncodingMode.Decode : EncodingMode.Encode); // Set new encoding } + + activeTextEditorControl.focus(); } } diff --git a/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css index 77a7fa03..366f6dbe 100644 --- a/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css @@ -14,7 +14,7 @@ flex: auto; } -.monaco-workbench .part.editor > .content .editor-group-container > .title .title-label { +.monaco-workbench .part.editor > .content .editor-group-container > .title > .label-container > .title-label { line-height: 35px; overflow: hidden; text-overflow: ellipsis; @@ -22,16 +22,16 @@ padding-left: 20px; } -.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .no-tabs.title-label { - flex: none; -} - -.monaco-workbench .part.editor > .content .editor-group-container > .title .monaco-icon-label::before { - height: 35px; /* tweak the icon size of the editor labels when icons are enabled */ +.monaco-workbench .part.editor > .content .editor-group-container > .title > .label-container > .title-label > .monaco-icon-label-container { + flex: none; /* helps to show decorations right next to the label and not at the end */ } /* Breadcrumbs */ +.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .no-tabs.title-label { + flex: none; +} + .monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control { flex: 1 50%; overflow: hidden; @@ -62,13 +62,11 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item.root_folder + .monaco-breadcrumb-item::before, .monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control.relative-path .monaco-breadcrumb-item:nth-child(2)::before, .monaco-workbench.windows .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:nth-child(2)::before { - /* workspace folder, item following workspace folder, or relative path -> hide first seperator */ - display: none; + display: none; /* workspace folder, item following workspace folder, or relative path -> hide first seperator */ } .monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item.root_folder::after { - /* use dot separator for workspace folder */ - content: '\00a0•\00a0'; + content: '\00a0•\00a0'; /* use dot separator for workspace folder */ padding: 0; } @@ -80,13 +78,18 @@ padding: 0 1px; } -.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item .codicon:last-child, -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child .codicon:last-child { - display: none; /* hides chevrons when no tabs visible and when last items */ +.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item .codicon:last-child { + display: none; /* hides chevrons when no tabs visible */ } -/* Title Actions */ -.monaco-workbench .part.editor > .content .editor-group-container > .title .title-actions { +.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-icon-label::before { + height: 18px; + padding-right: 2px; +} + +/* Editor Actions Toolbar (via title actions) */ + +.monaco-workbench .part.editor > .content .editor-group-container > .title > .title-actions { display: flex; flex: initial; opacity: 0.5; @@ -94,6 +97,6 @@ height: 35px; } -.monaco-workbench .part.editor > .content .editor-group-container.active > .title .title-actions { +.monaco-workbench .part.editor > .content .editor-group-container.active > .title > .title-actions { opacity: 1; } diff --git a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css index a9158b79..4dd7e77d 100644 --- a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css @@ -3,17 +3,45 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +/* + ################################### z-index explainer ################################### + + Tabs have various levels of z-index depending on state, typically: + - scrollbar should be above all + - sticky (compact, shrink) tabs need to be above non-sticky tabs for scroll under effect + including non-sticky tabs-top borders, otherwise these borders would not scroll under + (https://github.com/microsoft/vscode/issues/111641) + - bottom-border needs to be above tabs bottom border to win but also support sticky tabs + (https://github.com/microsoft/vscode/issues/99084) <- this currently cannot be done and + is still broken. putting sticky-tabs above tabs bottom border would not render this + border at all for sticky tabs. + + On top of that there is 2 borders with a z-index for a general border below tabs + - tabs bottom border + - editor title bottom border (when breadcrumbs are disabled, this border will appear + same as tabs bottom border) + + The following tabls shows the current stacking order: + + [z-index] [kind] + 7 scrollbar + 6 active-tab border-bottom + 5 tabs, title border bottom + 4 sticky-tab + 2 active/dirty-tab border top + 0 tab + + ########################################################################################## +*/ + /* Title Container */ -.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container { +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container { display: flex; + position: relative; /* position tabs border bottom or editor actions (when tabs wrap) relative to this container */ } -.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container.tabs-border-bottom { - position: relative; -} - -.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container.tabs-border-bottom::after { +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.tabs-border-bottom::after { content: ''; position: absolute; bottom: 0; @@ -25,12 +53,12 @@ height: 1px; } -.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container > .monaco-scrollable-element { +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container > .monaco-scrollable-element { flex: 1; } -.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container > .monaco-scrollable-element .scrollbar { - z-index: 3; /* on top of tabs */ +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container > .monaco-scrollable-element .scrollbar { + z-index: 7; cursor: default; } @@ -46,6 +74,13 @@ overflow: scroll !important; } +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container { + + /* Enable wrapping via flex layout and dynamic height */ + height: auto; + flex-wrap: wrap; +} + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container::-webkit-scrollbar { display: none; /* Chrome + Safari: hide scrollbar */ } @@ -62,6 +97,10 @@ padding-left: 10px; } +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab:last-child { + margin-right: var(--last-tab-margin-right); /* when tabs wrap, we need a margin away from the absolute positioned editor actions */ +} + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon.tab-actions-right, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon.tab-actions-off:not(.sticky-compact) { padding-left: 5px; /* reduce padding when we show icons and are in shrinking mode and tab actions is not left (unless sticky-compact) */ @@ -74,6 +113,10 @@ flex-shrink: 0; } +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab.sizing-fit { + flex-grow: 1; /* grow the tabs to fill each row for a more homogeneous look when tabs wrap */ +} + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink { min-width: 80px; flex-basis: 0; /* all tabs are even */ @@ -89,7 +132,7 @@ /** Sticky compact/shrink tabs do not scroll in case of overflow and are always above unsticky tabs which scroll under */ position: sticky; - z-index: 1; + z-index: 4; /** Sticky compact/shrink tabs are even and never grow */ flex-basis: 0; @@ -118,9 +161,7 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-shrink.sticky-compact, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-fit.sticky-shrink, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-shrink.sticky-shrink { - - /** Disable sticky positions for sticky compact/shrink tabs if the available space is too little */ - position: static; + position: static; /** disable sticky positions for sticky compact/shrink tabs if the available space is too little */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-left .action-label { @@ -132,7 +173,7 @@ content: ''; display: flex; flex: 0; - width: 5px; /* Reserve space to hide tab fade when close button is left or off (fixes https://github.com/microsoft/vscode/issues/45728) */ + width: 5px; /* reserve space to hide tab fade when close button is left or off (fixes https://github.com/microsoft/vscode/issues/45728) */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.tab-actions-left { @@ -167,24 +208,26 @@ display: block; position: absolute; left: 0; - z-index: 6; /* over possible title border */ pointer-events: none; width: 100%; } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container { + z-index: 2; top: 0; height: 1px; background-color: var(--tab-border-top-color); } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container { + z-index: 6; bottom: 0; height: 1px; background-color: var(--tab-border-bottom-color); } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty-border-top > .tab-border-top-container { + z-index: 2; top: 0; height: 2px; background-color: var(--tab-dirty-border-top-color); @@ -203,13 +246,16 @@ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .tab-label::after { - content: ''; + content: ''; /* enables a linear gradient to overlay the end of the label when tabs overflow */ position: absolute; right: 0; - height: 100%; width: 5px; opacity: 1; padding: 0; + /* the rules below ensure that the gradient does not impact top/bottom borders (https://github.com/microsoft/vscode/issues/115129) */ + top: 1px; + bottom: 1px; + height: calc(100% - 2px); } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink:focus > .tab-label::after { @@ -243,7 +289,7 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-right.sizing-shrink > .tab-actions { flex: 0; - overflow: hidden; /* let the tab actions be pushed out of view when sizing is set to shrink to make more room... */ + overflow: hidden; /* let the tab actions be pushed out of view when sizing is set to shrink to make more room */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty.tab-actions-right.sizing-shrink > .tab-actions, @@ -301,7 +347,7 @@ margin-right: 0.5em; } -/* No Tab Actions */ +/* Tab Actions: Off */ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-off { padding-right: 10px; /* give a little bit more room if tab actions is off */ @@ -324,15 +370,6 @@ pointer-events: none; /* don't allow tab actions to be clicked when running without tab actions */ } -/* Editor Actions */ - -.monaco-workbench .part.editor > .content .editor-group-container > .title .editor-actions { - cursor: default; - flex: initial; - padding: 0 8px 0 4px; - height: 35px; -} - /* Breadcrumbs */ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control { @@ -350,11 +387,17 @@ height: 22px; /* tweak the icon size of the editor labels when icons are enabled */ } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .outline-element-icon { + padding-right: 3px; + height: 22px; /* tweak the icon size of the editor labels when icons are enabled */ + line-height: 22px; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item { max-width: 80%; } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item::before { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item::before { width: 16px; height: 22px; display: flex; @@ -362,6 +405,27 @@ justify-content: center; } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child { padding-right: 8px; } + +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child .codicon:last-child { + display: none; /* hides chevrons when last item */ +} + +/* Editor Actions Toolbar */ + +.monaco-workbench .part.editor > .content .editor-group-container > .title .editor-actions { + cursor: default; + flex: initial; + padding: 0 8px 0 4px; + height: 35px; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .editor-actions { + + /* When tabs are wrapped, position the editor actions at the end of the very last row */ + position: absolute; + bottom: 0; + right: 0; +} diff --git a/src/vs/workbench/browser/parts/editor/media/titlecontrol.css b/src/vs/workbench/browser/parts/editor/media/titlecontrol.css index 59ab0d4a..420f4031 100644 --- a/src/vs/workbench/browser/parts/editor/media/titlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/titlecontrol.css @@ -26,6 +26,15 @@ cursor: pointer; } +.monaco-workbench .part.editor > .content .editor-group-container > .title .monaco-icon-label::before { + height: 35px; /* tweak the icon size of the editor labels when icons are enabled */ +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .monaco-icon-label::after, +.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .monaco-icon-label::after { + padding-right: 0; /* by default the icon label has a padding right and this isn't wanted when not showing tabs and not showing breadcrumbs */ +} + /* Title Actions */ .monaco-workbench .part.editor > .content .editor-group-container > .title .title-actions .action-label:not(span), @@ -59,13 +68,12 @@ opacity: 0.4; } -/* Drag Cursor */ +/* Drag and Drop */ + .monaco-workbench .part.editor > .content .editor-group-container > .title { cursor: grab; } -/* Drag and Drop Feedback */ - .monaco-editor-group-drag-image { display: inline-block; padding: 1px 7px; diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 66a7caec..c779550c 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/notabstitlecontrol'; import { EditorResourceAccessor, Verbosity, IEditorInput, IEditorPartOptions, SideBySideEditor } from 'vs/workbench/common/editor'; -import { TitleControl, IToolbarActions } from 'vs/workbench/browser/parts/editor/titleControl'; +import { TitleControl, IToolbarActions, ITitleControlDimensions } from 'vs/workbench/browser/parts/editor/titleControl'; import { ResourceLabel, IResourceLabel } from 'vs/workbench/browser/labels'; import { TAB_ACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/browser/touch'; @@ -15,6 +15,7 @@ import { CLOSE_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/edito import { Color } from 'vs/base/common/color'; import { withNullAsUndefined, assertIsDefined, assertAllDefined } from 'vs/base/common/types'; import { IEditorGroupTitleDimensions } from 'vs/workbench/browser/parts/editor/editor'; +import { equals } from 'vs/base/common/objects'; interface IRenderedEditorLabel { editor?: IEditorInput; @@ -50,7 +51,7 @@ export class NoTabsTitleControl extends TitleControl { // Breadcrumbs this.createBreadcrumbsControl(labelContainer, { showFileIcons: false, showSymbolIcons: true, showDecorationColors: false, breadcrumbsBackground: () => Color.transparent }); titleContainer.classList.toggle('breadcrumbs', Boolean(this.breadcrumbsControl)); - this._register({ dispose: () => titleContainer.classList.remove('breadcrumbs') }); // import to remove because the container is a shared dom node + this._register({ dispose: () => titleContainer.classList.remove('breadcrumbs') }); // important to remove because the container is a shared dom node // Right Actions Container const actionsContainer = document.createElement('div'); @@ -67,16 +68,16 @@ export class NoTabsTitleControl extends TitleControl { this.enableGroupDragging(titleContainer); // Pin on double click - this._register(addDisposableListener(titleContainer, EventType.DBLCLICK, (e: MouseEvent) => this.onTitleDoubleClick(e))); + this._register(addDisposableListener(titleContainer, EventType.DBLCLICK, e => this.onTitleDoubleClick(e))); // Detect mouse click - this._register(addDisposableListener(titleContainer, EventType.AUXCLICK, (e: MouseEvent) => this.onTitleAuxClick(e))); + this._register(addDisposableListener(titleContainer, EventType.AUXCLICK, e => this.onTitleAuxClick(e))); // Detect touch this._register(addDisposableListener(titleContainer, TouchEventType.Tap, (e: GestureEvent) => this.onTitleTap(e))); // Context Menu - this._register(addDisposableListener(titleContainer, EventType.CONTEXT_MENU, (e: Event) => { + this._register(addDisposableListener(titleContainer, EventType.CONTEXT_MENU, e => { if (this.group.activeEditor) { this.onContextMenu(this.group.activeEditor, e, titleContainer); } @@ -188,7 +189,7 @@ export class NoTabsTitleControl extends TitleControl { } updateOptions(oldOptions: IEditorPartOptions, newOptions: IEditorPartOptions): void { - if (oldOptions.labelFormat !== newOptions.labelFormat) { + if (oldOptions.labelFormat !== newOptions.labelFormat || !equals(oldOptions.decorations, newOptions.decorations)) { this.redraw(); } } @@ -236,6 +237,7 @@ export class NoTabsTitleControl extends TitleControl { private redraw(): void { const editor = withNullAsUndefined(this.group.activeEditor); + const options = this.accessor.partOptions; const isEditorPinned = editor ? this.group.isPinned(editor) : false; const isGroupActive = this.accessor.activeGroup === this.group; @@ -291,14 +293,18 @@ export class NoTabsTitleControl extends TitleControl { { title, italic: !isEditorPinned, - extraClasses: ['no-tabs', 'title-label'] + extraClasses: ['no-tabs', 'title-label'], + fileDecorations: { + colors: Boolean(options.decorations?.colors), + badges: Boolean(options.decorations?.badges) + }, } ); if (isGroupActive) { - editorLabel.element.style.color = this.getColor(TAB_ACTIVE_FOREGROUND) || ''; + titleContainer.style.color = this.getColor(TAB_ACTIVE_FOREGROUND) || ''; } else { - editorLabel.element.style.color = this.getColor(TAB_UNFOCUSED_ACTIVE_FOREGROUND) || ''; + titleContainer.style.color = this.getColor(TAB_UNFOCUSED_ACTIVE_FOREGROUND) || ''; } // Update Editor Actions Toolbar @@ -333,9 +339,11 @@ export class NoTabsTitleControl extends TitleControl { }; } - layout(dimension: Dimension): void { + layout(dimensions: ITitleControlDimensions): Dimension { if (this.breadcrumbsControl) { this.breadcrumbsControl.layout(undefined); } + + return new Dimension(dimensions.container.width, this.getDimensions().height); } } diff --git a/src/vs/workbench/browser/parts/editor/rangeDecorations.ts b/src/vs/workbench/browser/parts/editor/rangeDecorations.ts deleted file mode 100644 index 222d0fd5..00000000 --- a/src/vs/workbench/browser/parts/editor/rangeDecorations.ts +++ /dev/null @@ -1,121 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; -import { Emitter } from 'vs/base/common/event'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IRange } from 'vs/editor/common/core/range'; -import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; -import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { ICodeEditor, isCodeEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser'; -import { TrackedRangeStickiness, IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; -import { isEqual } from 'vs/base/common/resources'; - -export interface IRangeHighlightDecoration { - resource: URI; - range: IRange; - isWholeLine?: boolean; -} - -export class RangeHighlightDecorations extends Disposable { - - private rangeHighlightDecorationId: string | null = null; - private editor: ICodeEditor | null = null; - private readonly editorDisposables = this._register(new DisposableStore()); - - private readonly _onHighlightRemoved: Emitter = this._register(new Emitter()); - readonly onHighlightRemoved = this._onHighlightRemoved.event; - - constructor( - @IEditorService private readonly editorService: IEditorService - ) { - super(); - } - - removeHighlightRange() { - if (this.editor && this.editor.getModel() && this.rangeHighlightDecorationId) { - this.editor.deltaDecorations([this.rangeHighlightDecorationId], []); - this._onHighlightRemoved.fire(); - } - - this.rangeHighlightDecorationId = null; - } - - highlightRange(range: IRangeHighlightDecoration, editor?: any) { - editor = editor ?? this.getEditor(range); - if (isCodeEditor(editor)) { - this.doHighlightRange(editor, range); - } else if (isCompositeEditor(editor) && isCodeEditor(editor.activeCodeEditor)) { - this.doHighlightRange(editor.activeCodeEditor, range); - } - } - - private doHighlightRange(editor: ICodeEditor, selectionRange: IRangeHighlightDecoration) { - this.removeHighlightRange(); - - editor.changeDecorations((changeAccessor: IModelDecorationsChangeAccessor) => { - this.rangeHighlightDecorationId = changeAccessor.addDecoration(selectionRange.range, this.createRangeHighlightDecoration(selectionRange.isWholeLine)); - }); - - this.setEditor(editor); - } - - private getEditor(resourceRange: IRangeHighlightDecoration): ICodeEditor | undefined { - const activeEditor = this.editorService.activeEditor; - const resource = activeEditor && activeEditor.resource; - if (resource && isEqual(resource, resourceRange.resource)) { - return this.editorService.activeTextEditorControl as ICodeEditor; - } - - return undefined; - } - - private setEditor(editor: ICodeEditor) { - if (this.editor !== editor) { - this.editorDisposables.clear(); - this.editor = editor; - this.editorDisposables.add(this.editor.onDidChangeCursorPosition((e: ICursorPositionChangedEvent) => { - if ( - e.reason === CursorChangeReason.NotSet - || e.reason === CursorChangeReason.Explicit - || e.reason === CursorChangeReason.Undo - || e.reason === CursorChangeReason.Redo - ) { - this.removeHighlightRange(); - } - })); - this.editorDisposables.add(this.editor.onDidChangeModel(() => { this.removeHighlightRange(); })); - this.editorDisposables.add(this.editor.onDidDispose(() => { - this.removeHighlightRange(); - this.editor = null; - })); - } - } - - private static readonly _WHOLE_LINE_RANGE_HIGHLIGHT = ModelDecorationOptions.register({ - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - className: 'rangeHighlight', - isWholeLine: true - }); - - private static readonly _RANGE_HIGHLIGHT = ModelDecorationOptions.register({ - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - className: 'rangeHighlight' - }); - - private createRangeHighlightDecoration(isWholeLine: boolean = true): ModelDecorationOptions { - return (isWholeLine ? RangeHighlightDecorations._WHOLE_LINE_RANGE_HIGHLIGHT : RangeHighlightDecorations._RANGE_HIGHLIGHT); - } - - dispose() { - super.dispose(); - - if (this.editor && this.editor.getModel()) { - this.removeHighlightRange(); - this.editor = null; - } - } -} diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index bcd835f4..e6adbec2 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -18,19 +18,18 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IMenuService } from 'vs/platform/actions/common/actions'; -import { TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; +import { ITitleControlDimensions, TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IDisposable, dispose, DisposableStore, combinedDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { getOrSet } from 'vs/base/common/map'; -import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_FOREGROUND, TAB_INACTIVE_FOREGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND, TAB_UNFOCUSED_INACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, WORKBENCH_BACKGROUND, TAB_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, TAB_ACTIVE_MODIFIED_BORDER, TAB_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_ACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_BACKGROUND, TAB_HOVER_FOREGROUND, TAB_UNFOCUSED_HOVER_FOREGROUND, EDITOR_GROUP_HEADER_TABS_BORDER, TAB_LAST_PINNED_BORDER } from 'vs/workbench/common/theme'; import { activeContrastBorder, contrastBorder, editorBackground, breadcrumbsBackground } from 'vs/platform/theme/common/colorRegistry'; import { ResourcesDropHandler, DraggedEditorIdentifier, DraggedEditorGroupIdentifier, DragAndDropObserver } from 'vs/workbench/browser/dnd'; 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, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; @@ -48,6 +47,7 @@ import { IPath, win32, posix } from 'vs/base/common/path'; import { insert } from 'vs/base/common/arrays'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { isSafari } from 'vs/base/browser/browser'; +import { equals } from 'vs/base/common/objects'; interface IEditorInputLabel { name?: string; @@ -73,6 +73,9 @@ export class TabsTitleControl extends TitleControl { private static readonly TAB_HEIGHT = 35; + private static readonly MOUSE_WHEEL_EVENT_THRESHOLD = 150; + private static readonly MOUSE_WHEEL_DISTANCE_THRESHOLD = 1.5; + private titleContainer: HTMLElement | undefined; private tabsAndActionsContainer: HTMLElement | undefined; private tabsContainer: HTMLElement | undefined; @@ -87,12 +90,18 @@ export class TabsTitleControl extends TitleControl { private tabActionBars: ActionBar[] = []; private tabDisposables: IDisposable[] = []; - private dimension: Dimension | undefined; + private dimensions: ITitleControlDimensions & { used?: Dimension } = { + container: Dimension.None, + available: Dimension.None + }; + private readonly layoutScheduled = this._register(new MutableDisposable()); private blockRevealActiveTab: boolean | undefined; private path: IPath = isWindows ? win32 : posix; + private lastMouseWheelEventTime = 0; + constructor( parent: HTMLElement, accessor: IEditorGroupsAccessor, @@ -106,19 +115,21 @@ export class TabsTitleControl extends TitleControl { @IMenuService menuService: IMenuService, @IQuickInputService quickInputService: IQuickInputService, @IThemeService themeService: IThemeService, - @IExtensionService extensionService: IExtensionService, @IConfigurationService configurationService: IConfigurationService, @IFileService fileService: IFileService, @IEditorService private readonly editorService: EditorServiceImpl, @IPathService private readonly pathService: IPathService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService ) { - super(parent, accessor, group, contextMenuService, instantiationService, contextKeyService, keybindingService, telemetryService, notificationService, menuService, quickInputService, themeService, extensionService, configurationService, fileService); + super(parent, accessor, group, contextMenuService, instantiationService, contextKeyService, keybindingService, telemetryService, notificationService, menuService, quickInputService, themeService, configurationService, fileService); // Resolve the correct path library for the OS we are on // If we are connected to remote, this accounts for the // remote OS. (async () => this.path = await this.pathService.path)(); + + // React to decorations changing for our resource labels + this._register(this.tabResourceLabels.onDidChangeDecorations(() => this.doHandleDecorationsChange())); } protected create(parent: HTMLElement): void { @@ -151,7 +162,7 @@ export class TabsTitleControl extends TitleControl { // Editor Actions Toolbar this.createEditorActionsToolBar(this.editorToolbarContainer); - // Breadcrumbs (are on a separate row below tabs and actions) + // Breadcrumbs const breadcrumbsContainer = document.createElement('div'); breadcrumbsContainer.classList.add('tabs-breadcrumbs'); this.titleContainer.appendChild(breadcrumbsContainer); @@ -242,7 +253,7 @@ export class TabsTitleControl extends TitleControl { }); // Prevent auto-scrolling (https://github.com/microsoft/vscode/issues/16690) - this._register(addDisposableListener(tabsContainer, EventType.MOUSE_DOWN, (e: MouseEvent) => { + this._register(addDisposableListener(tabsContainer, EventType.MOUSE_DOWN, e => { if (e.button === 1) { e.preventDefault(); } @@ -337,8 +348,27 @@ export class TabsTitleControl extends TitleControl { } } - // Figure out scrolling direction - const nextEditor = this.group.getEditorByIndex(this.group.getIndexOfEditor(activeEditor) + (e.deltaX < 0 || e.deltaY < 0 /* scrolling up */ ? -1 : 1)); + // Ignore event if the last one happened too recently (https://github.com/microsoft/vscode/issues/96409) + // The restriction is relaxed according to the absolute value of `deltaX` and `deltaY` + // to support discrete (mouse wheel) and contiguous scrolling (touchpad) equally well + const now = Date.now(); + if (now - this.lastMouseWheelEventTime < TabsTitleControl.MOUSE_WHEEL_EVENT_THRESHOLD - 2 * (Math.abs(e.deltaX) + Math.abs(e.deltaY))) { + return; + } + + this.lastMouseWheelEventTime = now; + + // Figure out scrolling direction but ignore it if too subtle + let tabSwitchDirection: number; + if (e.deltaX + e.deltaY < - TabsTitleControl.MOUSE_WHEEL_DISTANCE_THRESHOLD) { + tabSwitchDirection = -1; + } else if (e.deltaX + e.deltaY > TabsTitleControl.MOUSE_WHEEL_DISTANCE_THRESHOLD) { + tabSwitchDirection = 1; + } else { + return; + } + + const nextEditor = this.group.getEditorByIndex(this.group.getIndexOfEditor(activeEditor) + tabSwitchDirection); if (!nextEditor) { return; } @@ -351,12 +381,19 @@ export class TabsTitleControl extends TitleControl { })); } + private doHandleDecorationsChange(): void { + + // A change to decorations potentially has an impact on the size of tabs + // so we need to trigger a layout in that case to adjust things + this.layout(this.dimensions); + } + protected updateEditorActionsToolbar(): void { super.updateEditorActionsToolbar(); // Changing the actions in the toolbar can have an impact on the size of the // tab container, so we need to layout the tabs to make sure the active is visible - this.layout(this.dimension); + this.layout(this.dimensions); } openEditor(editor: IEditorInput): void { @@ -439,7 +476,7 @@ export class TabsTitleControl extends TitleControl { }); // Moving an editor requires a layout to keep the active editor visible - this.layout(this.dimension); + this.layout(this.dimensions); } pinEditor(editor: IEditorInput): void { @@ -466,7 +503,7 @@ export class TabsTitleControl extends TitleControl { }); // A change to the sticky state requires a layout to keep the active editor visible - this.layout(this.dimension); + this.layout(this.dimensions); } setActive(isGroupActive: boolean): void { @@ -478,7 +515,7 @@ export class TabsTitleControl extends TitleControl { // Activity has an impact on the toolbar, so we need to update and layout this.updateEditorActionsToolbar(); - this.layout(this.dimension); + this.layout(this.dimensions); } private updateEditorLabelAggregator = this._register(new RunOnceScheduler(() => this.updateEditorLabels(), 0)); @@ -504,7 +541,7 @@ export class TabsTitleControl extends TitleControl { }); // A change to a label requires a layout to keep the active editor visible - this.layout(this.dimension); + this.layout(this.dimensions); } updateEditorDirty(editor: IEditorInput): void { @@ -531,7 +568,9 @@ export class TabsTitleControl extends TitleControl { oldOptions.pinnedTabSizing !== newOptions.pinnedTabSizing || oldOptions.showIcons !== newOptions.showIcons || oldOptions.hasIcons !== newOptions.hasIcons || - oldOptions.highlightModifiedTabs !== newOptions.highlightModifiedTabs + oldOptions.highlightModifiedTabs !== newOptions.highlightModifiedTabs || + oldOptions.wrapTabs !== newOptions.wrapTabs || + !equals(oldOptions.decorations, newOptions.decorations) ) { this.redraw(); } @@ -648,7 +687,7 @@ export class TabsTitleControl extends TitleControl { }; // Open on Click / Touch - disposables.add(addDisposableListener(tab, EventType.MOUSE_DOWN, (e: MouseEvent) => handleClickOrTouch(e))); + disposables.add(addDisposableListener(tab, EventType.MOUSE_DOWN, e => handleClickOrTouch(e))); disposables.add(addDisposableListener(tab, TouchEventType.Tap, (e: GestureEvent) => handleClickOrTouch(e))); // Touch Scroll Support @@ -657,14 +696,14 @@ export class TabsTitleControl extends TitleControl { })); // Prevent flicker of focus outline on tab until editor got focus - disposables.add(addDisposableListener(tab, EventType.MOUSE_UP, (e: MouseEvent) => { + disposables.add(addDisposableListener(tab, EventType.MOUSE_UP, e => { EventHelper.stop(e); tab.blur(); })); // Close on mouse middle click - disposables.add(addDisposableListener(tab, EventType.AUXCLICK, (e: MouseEvent) => { + disposables.add(addDisposableListener(tab, EventType.AUXCLICK, e => { if (e.button === 1 /* Middle Button*/) { EventHelper.stop(e, true /* for https://github.com/microsoft/vscode/issues/56715 */); @@ -674,7 +713,7 @@ export class TabsTitleControl extends TitleControl { })); // Context menu on Shift+F10 - disposables.add(addDisposableListener(tab, EventType.KEY_DOWN, (e: KeyboardEvent) => { + disposables.add(addDisposableListener(tab, EventType.KEY_DOWN, e => { const event = new StandardKeyboardEvent(e); if (event.shiftKey && event.keyCode === KeyCode.F10) { showContextMenu(e); @@ -687,7 +726,7 @@ export class TabsTitleControl extends TitleControl { })); // Keyboard accessibility - disposables.add(addDisposableListener(tab, EventType.KEY_UP, (e: KeyboardEvent) => { + disposables.add(addDisposableListener(tab, EventType.KEY_UP, e => { const event = new StandardKeyboardEvent(e); let handled = false; @@ -750,7 +789,7 @@ export class TabsTitleControl extends TitleControl { }); // Context menu - disposables.add(addDisposableListener(tab, EventType.CONTEXT_MENU, (e: Event) => { + disposables.add(addDisposableListener(tab, EventType.CONTEXT_MENU, e => { EventHelper.stop(e, true); const input = this.group.getEditorByIndex(index); @@ -760,7 +799,7 @@ export class TabsTitleControl extends TitleControl { }, true /* use capture to fix https://github.com/microsoft/vscode/issues/19145 */)); // Drag support - disposables.add(addDisposableListener(tab, EventType.DRAG_START, (e: DragEvent) => { + disposables.add(addDisposableListener(tab, EventType.DRAG_START, e => { const editor = this.group.getEditorByIndex(index); if (!editor) { return; @@ -1022,7 +1061,7 @@ export class TabsTitleControl extends TitleControl { this.updateEditorActionsToolbar(); // Ensure the active tab is always revealed - this.layout(this.dimension); + this.layout(this.dimensions); } private redrawTab(editor: IEditorInput, index: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel, tabActionBar: ActionBar): void { @@ -1092,12 +1131,14 @@ export class TabsTitleControl extends TitleControl { // or their first character of the name otherwise let name: string | undefined; let forceLabel = false; + let forceDisableBadgeDecorations = false; let description: string; if (options.pinnedTabSizing === 'compact' && this.group.isSticky(index)) { const isShowingIcons = options.showIcons && options.hasIcons; name = isShowingIcons ? '' : tabLabel.name?.charAt(0).toUpperCase(); description = ''; forceLabel = true; + forceDisableBadgeDecorations = true; // not enough space when sticky tabs are compact } else { name = tabLabel.name; description = tabLabel.description || ''; @@ -1116,7 +1157,16 @@ export class TabsTitleControl extends TitleControl { // Label tabLabelWidget.setResource( { name, description, resource: EditorResourceAccessor.getOriginalUri(editor, { supportSideBySide: SideBySideEditor.BOTH }) }, - { title, extraClasses: ['tab-label'], italic: !this.group.isPinned(editor), forceLabel } + { + title, + extraClasses: ['tab-label'], + italic: !this.group.isPinned(editor), + forceLabel, + fileDecorations: { + colors: Boolean(options.decorations?.colors), + badges: forceDisableBadgeDecorations ? false : Boolean(options.decorations?.badges) + } + } ); // Tests helper @@ -1234,62 +1284,179 @@ export class TabsTitleControl extends TitleControl { } getDimensions(): IEditorGroupTitleDimensions { - let height = TabsTitleControl.TAB_HEIGHT; - if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { - height += BreadcrumbsControl.HEIGHT; + let height: number; + + // Wrap: we need to ask `offsetHeight` to get + // the real height of the title area with wrapping. + if (this.accessor.partOptions.wrapTabs && this.tabsAndActionsContainer?.classList.contains('wrapping')) { + height = this.tabsAndActionsContainer.offsetHeight; + } else { + height = TabsTitleControl.TAB_HEIGHT; } - return { - height, - offset: TabsTitleControl.TAB_HEIGHT - }; + const offset = height; + + // Account for breadcrumbs if visible + if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { + height += BreadcrumbsControl.HEIGHT; // Account for breadcrumbs if visible + } + + return { height, offset }; } - layout(dimension: Dimension | undefined): void { - this.dimension = dimension; + layout(dimensions: ITitleControlDimensions): Dimension { - const activeTabAndIndex = this.group.activeEditor ? this.getTabAndIndex(this.group.activeEditor) : undefined; - if (!activeTabAndIndex || !this.dimension) { - return; - } + // Remember dimensions that we get + Object.assign(this.dimensions, dimensions); // The layout of tabs can be an expensive operation because we access DOM properties // that can result in the browser doing a full page layout to validate them. To buffer // this a little bit we try at least to schedule this work on the next animation frame. if (!this.layoutScheduled.value) { this.layoutScheduled.value = scheduleAtNextAnimationFrame(() => { - const dimension = assertIsDefined(this.dimension); - this.doLayout(dimension); + this.doLayout(this.dimensions); this.layoutScheduled.clear(); }); } + + // First time layout: compute the dimensions and store it + if (!this.dimensions.used) { + this.dimensions.used = new Dimension(dimensions.container.width, this.getDimensions().height); + } + + return this.dimensions.used; } - private doLayout(dimension: Dimension): void { + private doLayout(dimensions: ITitleControlDimensions): void { + + // Only layout if we have valid tab index and dimensions const activeTabAndIndex = this.group.activeEditor ? this.getTabAndIndex(this.group.activeEditor) : undefined; - if (!activeTabAndIndex) { - return; // nothing to do if not editor opened + if (activeTabAndIndex && dimensions.container !== Dimension.None && dimensions.available !== Dimension.None) { + + // Breadcrumbs + this.doLayoutBreadcrumbs(dimensions); + + // Tabs + const [activeTab, activeIndex] = activeTabAndIndex; + this.doLayoutTabs(activeTab, activeIndex, dimensions); } - // Breadcrumbs - this.doLayoutBreadcrumbs(dimension); + // Remember the dimensions used in the control so that we can + // return it fast from the `layout` call without having to + // compute it over and over again + const oldDimension = this.dimensions.used; + const newDimension = this.dimensions.used = new Dimension(dimensions.container.width, this.getDimensions().height); - // Tabs - const [activeTab, activeIndex] = activeTabAndIndex; - this.doLayoutTabs(activeTab, activeIndex); + // In case the height of the title control changed from before + // (currently only possible if wrapping changed on/off), we need + // to signal this to the outside via a `relayout` call so that + // e.g. the editor control can be adjusted accordingly. + if (oldDimension && oldDimension.height !== newDimension.height) { + this.group.relayout(); + } } - private doLayoutBreadcrumbs(dimension: Dimension): void { + private doLayoutBreadcrumbs(dimensions: ITitleControlDimensions): void { if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { - const tabsScrollbar = assertIsDefined(this.tabsScrollbar); - - this.breadcrumbsControl.layout(new Dimension(dimension.width, BreadcrumbsControl.HEIGHT)); - tabsScrollbar.getDomNode().style.height = `${dimension.height - BreadcrumbsControl.HEIGHT}px`; + this.breadcrumbsControl.layout(new Dimension(dimensions.container.width, BreadcrumbsControl.HEIGHT)); } } - private doLayoutTabs(activeTab: HTMLElement, activeIndex: number): void { + private doLayoutTabs(activeTab: HTMLElement, activeIndex: number, dimensions: ITitleControlDimensions): void { + + // Always first layout tabs with wrapping support even if wrapping + // is disabled. The result indicates if tabs wrap and if not, we + // need to proceed with the layout without wrapping because even + // if wrapping is enabled in settings, there are cases where + // wrapping is disabled (e.g. due to space constraints) + const tabsWrapMultiLine = this.doLayoutTabsWrapping(dimensions); + if (!tabsWrapMultiLine) { + this.doLayoutTabsNonWrapping(activeTab, activeIndex); + } + } + + private doLayoutTabsWrapping(dimensions: ITitleControlDimensions): boolean { + const [tabsAndActionsContainer, tabsContainer, editorToolbarContainer, tabsScrollbar] = assertAllDefined(this.tabsAndActionsContainer, this.tabsContainer, this.editorToolbarContainer, this.tabsScrollbar); + + // Handle wrapping tabs according to setting: + // - enabled: only add class if tabs wrap and don't exceed available dimensions + // - disabled: remove class and margin-right variable + + const didTabsWrapMultiLine = tabsAndActionsContainer.classList.contains('wrapping'); + let tabsWrapMultiLine = didTabsWrapMultiLine; + + function updateTabsWrapping(enabled: boolean): void { + tabsWrapMultiLine = enabled; + + // Toggle the `wrapped` class to enable wrapping + tabsAndActionsContainer.classList.toggle('wrapping', tabsWrapMultiLine); + + // Update `last-tab-margin-right` CSS variable to account for the absolute + // positioned editor actions container when tabs wrap. The margin needs to + // be the width of the editor actions container to avoid screen cheese. + tabsContainer.style.setProperty('--last-tab-margin-right', tabsWrapMultiLine ? `${editorToolbarContainer.offsetWidth}px` : '0'); + } + + // Setting enabled: selectively enable wrapping if possible + if (this.accessor.partOptions.wrapTabs) { + const visibleTabsWidth = tabsContainer.offsetWidth; + const allTabsWidth = tabsContainer.scrollWidth; + const lastTabFitsWrapped = () => { + const lastTab = this.getLastTab(); + if (!lastTab) { + return true; // no tab always fits + } + + return lastTab.offsetWidth <= (dimensions.available.width - editorToolbarContainer.offsetWidth); + }; + + // If tabs wrap or should start to wrap (when width exceeds visible width) + // we must trigger `updateWrapping` to set the `last-tab-margin-right` + // accordingly based on the number of actions. The margin is important to + // properly position the last tab apart from the actions + // + // We already check here if the last tab would fit when wrapped given the + // editor toolbar will also show right next to it. This ensures we are not + // enabling wrapping only to disable it again in the code below (this fixes + // flickering issue https://github.com/microsoft/vscode/issues/115050) + if (tabsWrapMultiLine || (allTabsWidth > visibleTabsWidth && lastTabFitsWrapped())) { + updateTabsWrapping(true); + } + + // Tabs wrap multiline: remove wrapping under certain size constraint conditions + if (tabsWrapMultiLine) { + if ( + (tabsContainer.offsetHeight > dimensions.available.height) || // if height exceeds available height + (allTabsWidth === visibleTabsWidth && tabsContainer.offsetHeight === TabsTitleControl.TAB_HEIGHT) || // if wrapping is not needed anymore + (!lastTabFitsWrapped()) // if last tab does not fit anymore + ) { + updateTabsWrapping(false); + } + } + } + + // Setting disabled: remove CSS traces only if tabs did wrap + else if (didTabsWrapMultiLine) { + updateTabsWrapping(false); + } + + // If we transitioned from non-wrapping to wrapping, we need + // to update the scrollbar to have an equal `width` and + // `scrollWidth`. Otherwise a scrollbar would appear which is + // never desired when wrapping. + if (tabsWrapMultiLine && !didTabsWrapMultiLine) { + const visibleTabsWidth = tabsContainer.offsetWidth; + tabsScrollbar.setScrollDimensions({ + width: visibleTabsWidth, + scrollWidth: visibleTabsWidth + }); + } + + return tabsWrapMultiLine; + } + + private doLayoutTabsNonWrapping(activeTab: HTMLElement, activeIndex: number): void { const [tabsContainer, tabsScrollbar] = assertAllDefined(this.tabsContainer, this.tabsScrollbar); // @@ -1308,7 +1475,7 @@ export class TabsTitleControl extends TitleControl { // [-- Sticky Tabs Width --] // - const visibleTabsContainerWidth = tabsContainer.offsetWidth; + const visibleTabsWidth = tabsContainer.offsetWidth; const allTabsWidth = tabsContainer.scrollWidth; // Compute width of sticky tabs depending on pinned tab sizing @@ -1331,17 +1498,17 @@ export class TabsTitleControl extends TitleControl { } // Figure out if active tab is positioned static which has an - // impact on wether to reveal the tab or not later + // impact on whether to reveal the tab or not later let activeTabPositionStatic = this.accessor.partOptions.pinnedTabSizing !== 'normal' && this.group.isSticky(activeIndex); // Special case: we have sticky tabs but the available space for showing tabs // is little enough that we need to disable sticky tabs sticky positioning // so that tabs can be scrolled at naturally. - let availableTabsContainerWidth = visibleTabsContainerWidth - stickyTabsWidth; + let availableTabsContainerWidth = visibleTabsWidth - stickyTabsWidth; if (this.group.stickyCount > 0 && availableTabsContainerWidth < TabsTitleControl.TAB_WIDTH.fit) { tabsContainer.classList.add('disable-sticky-tabs'); - availableTabsContainerWidth = visibleTabsContainerWidth; + availableTabsContainerWidth = visibleTabsWidth; stickyTabsWidth = 0; activeTabPositionStatic = false; } else { @@ -1358,7 +1525,7 @@ export class TabsTitleControl extends TitleControl { // Update scrollbar tabsScrollbar.setScrollDimensions({ - width: visibleTabsContainerWidth, + width: visibleTabsWidth, scrollWidth: allTabsWidth }); @@ -1426,15 +1593,28 @@ export class TabsTitleControl extends TitleControl { private getTabAndIndex(editor: IEditorInput): [HTMLElement, number /* index */] | undefined { const editorIndex = this.group.getIndexOfEditor(editor); - if (editorIndex >= 0) { - const tabsContainer = assertIsDefined(this.tabsContainer); - - return [tabsContainer.children[editorIndex] as HTMLElement, editorIndex]; + const tab = this.getTabAtIndex(editorIndex); + if (tab) { + return [tab, editorIndex]; } return undefined; } + private getTabAtIndex(editorIndex: number): HTMLElement | undefined { + if (editorIndex >= 0) { + const tabsContainer = assertIsDefined(this.tabsContainer); + + return tabsContainer.children[editorIndex] as HTMLElement | undefined; + } + + return undefined; + } + + private getLastTab(): HTMLElement | undefined { + return this.getTabAtIndex(this.group.count - 1); + } + private blockRevealActiveTabOnce(): void { // When closing tabs through the tab close button or gesture, the user @@ -1527,16 +1707,28 @@ export class TabsTitleControl extends TitleControl { } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { // Add border between tabs and breadcrumbs in high contrast mode. if (theme.type === ColorScheme.HIGH_CONTRAST) { const borderColor = (theme.getColor(TAB_BORDER) || theme.getColor(contrastBorder)); + if (borderColor) { + collector.addRule(` + .monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container { + border-bottom: 1px solid ${borderColor}; + } + `); + } + } + + // Add bottom border to tabs when wrapping + const borderColor = theme.getColor(TAB_BORDER); + if (borderColor) { collector.addRule(` - .monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container { - border-bottom: 1px solid ${borderColor}; - } - `); + .monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab { + border-bottom: 1px solid ${borderColor}; + } + `); } // Styling with Outline color (e.g. high contrast theme) diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 4557a2c4..7ae3677e 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import * as objects from 'vs/base/common/objects'; -import { isFunction, isObject, isArray, assertIsDefined } from 'vs/base/common/types'; +import { isFunction, isObject, isArray, assertIsDefined, withUndefinedAsNull } from 'vs/base/common/types'; import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IDiffEditorOptions, IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BaseTextEditor, IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; @@ -103,7 +103,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan } createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): IDiffEditor { - return this.instantiationService.createInstance(DiffEditorWidget, parent, configuration); + return this.instantiationService.createInstance(DiffEditorWidget, parent, configuration, {}); } async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { @@ -132,8 +132,8 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan // Set Editor Model const diffEditor = assertIsDefined(this.getControl()); - const resolvedDiffEditorModel = resolvedModel; - diffEditor.setModel(resolvedDiffEditorModel.textDiffEditorModel); + const resolvedDiffEditorModel = resolvedModel as TextDiffEditorModel; + diffEditor.setModel(withUndefinedAsNull(resolvedDiffEditorModel.textDiffEditorModel)); // Apply Options from TextOptions let optionsGotApplied = false; diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index 9cae4d21..7ec16141 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -233,7 +233,6 @@ export abstract class BaseTextEditor extends EditorPane implements ITextEditorPa return undefined; } - protected saveTextEditorViewState(resource: URI, cleanUpOnDispose?: IEditorInput): void { const editorViewState = this.retrieveTextEditorViewState(resource); if (!editorViewState || !this.group) { diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index 8358dfcb..6b7e1e5e 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -9,14 +9,13 @@ import { addDisposableListener, Dimension, EventType } from 'vs/base/browser/dom import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { ActionsOrientation, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IAction, IRunEvent, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, IActionViewItem } from 'vs/base/common/actions'; -import * as arrays from 'vs/base/common/arrays'; +import { IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, IActionViewItem } from 'vs/base/common/actions'; import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; import { dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { localize } from 'vs/nls'; -import { createAndFillInActionBarActions, createAndFillInContextMenuActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { ExecuteCommandAction, IMenu, IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { createActionViewItem, createAndFillInActionBarActions, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -26,7 +25,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; -import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant, Themable } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant, Themable } from 'vs/platform/theme/common/themeService'; import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, fillResourceDataTransfers, LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; @@ -34,7 +33,6 @@ import { BreadcrumbsControl, IBreadcrumbsControlOptions } from 'vs/workbench/bro import { IEditorGroupsAccessor, IEditorGroupTitleDimensions, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { EditorCommandsContextActionRunner, IEditorCommandsContext, IEditorInput, EditorResourceAccessor, IEditorPartOptions, SideBySideEditor, ActiveEditorPinnedContext, ActiveEditorStickyContext } from 'vs/workbench/common/editor'; import { ResourceContextKey } from 'vs/workbench/common/resources'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { IFileService } from 'vs/platform/files/common/files'; import { withNullAsUndefined, withUndefinedAsNull, assertIsDefined } from 'vs/base/common/types'; @@ -46,6 +44,20 @@ export interface IToolbarActions { secondary: IAction[]; } +export interface ITitleControlDimensions { + + /** + * The size of the parent container the title control is layed out in. + */ + container: Dimension; + + /** + * The maximum size the title control is allowed to consume based on + * other controls that are positioned inside the container. + */ + available: Dimension; +} + export abstract class TitleControl extends Themable { protected readonly groupTransfer = LocalSelectionTransfer.getInstance(); @@ -53,9 +65,6 @@ export abstract class TitleControl extends Themable { protected breadcrumbsControl: BreadcrumbsControl | undefined = undefined; - private currentPrimaryEditorActionIds: string[] = []; - private currentSecondaryEditorActionIds: string[] = []; - private editorActionsToolbar: ToolBar | undefined; private resourceContext: ResourceContextKey; @@ -79,7 +88,6 @@ export abstract class TitleControl extends Themable { @IMenuService private readonly menuService: IMenuService, @IQuickInputService protected quickInputService: IQuickInputService, @IThemeService themeService: IThemeService, - @IExtensionService private readonly extensionService: IExtensionService, @IConfigurationService protected configurationService: IConfigurationService, @IFileService private readonly fileService: IFileService ) { @@ -92,13 +100,6 @@ export abstract class TitleControl extends Themable { this.contextMenu = this._register(this.menuService.createMenu(MenuId.EditorTitleContext, this.contextKeyService)); this.create(parent); - this.registerListeners(); - } - - protected registerListeners(): void { - - // Update actions toolbar when extension register that may contribute them - this._register(this.extensionService.onDidRegisterExtensions(() => this.updateEditorActionsToolbar())); } protected abstract create(parent: HTMLElement): void; @@ -147,7 +148,7 @@ export abstract class TitleControl extends Themable { this.editorActionsToolbar.context = context; // Action Run Handling - this._register(this.editorActionsToolbar.actionRunner.onDidRun((e: IRunEvent) => { + this._register(this.editorActionsToolbar.actionRunner.onDidRun(e => { // Notify for Error this.notificationService.error(e.error); @@ -172,35 +173,14 @@ export abstract class TitleControl extends Themable { } // Check extensions - if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action); - } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); - } - - return undefined; + return createActionViewItem(this.instantiationService, action); } protected updateEditorActionsToolbar(): void { - - // Update Editor Actions Toolbar const { primaryEditorActions, secondaryEditorActions } = this.prepareEditorActions(this.getEditorActions()); - // Only update if something actually has changed - const primaryEditorActionIds = primaryEditorActions.map(a => a.id); - const secondaryEditorActionIds = secondaryEditorActions.map(a => a.id); - if ( - !arrays.equals(primaryEditorActionIds, this.currentPrimaryEditorActionIds) || - !arrays.equals(secondaryEditorActionIds, this.currentSecondaryEditorActionIds) || - primaryEditorActions.some(action => action instanceof ExecuteCommandAction) || // execute command actions can have the same ID but different arguments - secondaryEditorActions.some(action => action instanceof ExecuteCommandAction) // see also https://github.com/microsoft/vscode/issues/16298 - ) { - const editorActionsToolbar = assertIsDefined(this.editorActionsToolbar); - editorActionsToolbar.setActions(primaryEditorActions, secondaryEditorActions); - - this.currentPrimaryEditorActionIds = primaryEditorActionIds; - this.currentSecondaryEditorActionIds = secondaryEditorActionIds; - } + const editorActionsToolbar = assertIsDefined(this.editorActionsToolbar); + editorActionsToolbar.setActions(primaryEditorActions, secondaryEditorActions); } protected prepareEditorActions(editorActions: IToolbarActions): { primaryEditorActions: IAction[]; secondaryEditorActions: IAction[]; } { @@ -251,18 +231,13 @@ export abstract class TitleControl extends Themable { } protected clearEditorActionsToolbar(): void { - if (this.editorActionsToolbar) { - this.editorActionsToolbar.setActions([], []); - } - - this.currentPrimaryEditorActionIds = []; - this.currentSecondaryEditorActionIds = []; + this.editorActionsToolbar?.setActions([], []); } protected enableGroupDragging(element: HTMLElement): void { // Drag start - this._register(addDisposableListener(element, EventType.DRAG_START, (e: DragEvent) => { + this._register(addDisposableListener(element, EventType.DRAG_START, e => { if (e.target !== element) { return; // only if originating from tabs container } @@ -354,7 +329,7 @@ export abstract class TitleControl extends Themable { getAnchor: () => anchor, getActions: () => actions, getActionsContext: () => ({ groupId: this.group.id, editorIndex: this.group.getIndexOfEditor(editor) }), - getKeyBinding: (action) => this.getKeybinding(action), + getKeyBinding: action => this.getKeybinding(action), onHide: () => { // restore previous contexts @@ -407,7 +382,7 @@ export abstract class TitleControl extends Themable { abstract updateStyles(): void; - abstract layout(dimension: Dimension): void; + abstract layout(dimensions: ITitleControlDimensions): Dimension; abstract getDimensions(): IEditorGroupTitleDimensions; @@ -419,7 +394,7 @@ export abstract class TitleControl extends Themable { } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { // Drag Feedback const dragImageBackground = theme.getColor(listActiveSelectionBackground); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts index d4074bbe..8113a026 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/notificationsCenter'; import 'vs/css!./media/notificationsActions'; import { NOTIFICATIONS_BORDER, NOTIFICATIONS_CENTER_HEADER_FOREGROUND, NOTIFICATIONS_CENTER_HEADER_BACKGROUND, NOTIFICATIONS_CENTER_BORDER } from 'vs/workbench/common/theme'; -import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector, Themable } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant, Themable } from 'vs/platform/theme/common/themeService'; import { INotificationsModel, INotificationChangeEvent, NotificationChangeType, NotificationViewItemContentChangeKind } from 'vs/workbench/common/notifications'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { Emitter } from 'vs/base/common/event'; @@ -313,7 +313,7 @@ export class NotificationsCenter extends Themable implements INotificationsCente } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { const notificationBorderColor = theme.getColor(NOTIFICATIONS_BORDER); if (notificationBorderColor) { collector.addRule(`.monaco-workbench > .notifications-center .notifications-list-container .monaco-list-row[data-last-element="false"] > .notification-list-item { border-bottom: 1px solid ${notificationBorderColor}; }`); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsList.ts b/src/vs/workbench/browser/parts/notifications/notificationsList.ts index 7cbfb84f..b1e8112e 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsList.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsList.ts @@ -10,7 +10,7 @@ import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IListOptions } from 'vs/base/browser/ui/list/listWidget'; import { NOTIFICATIONS_LINKS, NOTIFICATIONS_BACKGROUND, NOTIFICATIONS_FOREGROUND, NOTIFICATIONS_ERROR_ICON_FOREGROUND, NOTIFICATIONS_WARNING_ICON_FOREGROUND, NOTIFICATIONS_INFO_ICON_FOREGROUND } from 'vs/workbench/common/theme'; -import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector, Themable } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant, Themable } from 'vs/platform/theme/common/themeService'; import { contrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry'; import { INotificationViewItem } from 'vs/workbench/common/notifications'; import { NotificationsListDelegate, NotificationRenderer } from 'vs/workbench/browser/parts/notifications/notificationsViewer'; @@ -278,7 +278,7 @@ export class NotificationsList extends Themable { } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { const linkColor = theme.getColor(NOTIFICATIONS_LINKS); if (linkColor) { collector.addRule(`.monaco-workbench .notifications-list-container .notification-list-item .notification-list-item-message a { color: ${linkColor}; }`); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index 51272ab8..2c8b0f46 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -470,7 +470,9 @@ export class NotificationTemplateRenderer extends Disposable { : buttonToolbar.addButton(buttonOptions)); button.label = action.label; this.inputDisposables.add(button.onDidClick(e => { - EventHelper.stop(e, true); + if (e) { + EventHelper.stop(e, true); + } actionRunner.run(action); })); diff --git a/src/vs/workbench/browser/parts/panel/panelActions.ts b/src/vs/workbench/browser/parts/panel/panelActions.ts index 74619ece..372e78f8 100644 --- a/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/src/vs/workbench/browser/parts/panel/panelActions.ts @@ -5,45 +5,26 @@ import 'vs/css!./media/panelpart'; import * as nls from 'vs/nls'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { Action } from 'vs/base/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { SyncActionDescriptor, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuId, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as WorkbenchExtensions, CATEGORIES } from 'vs/workbench/common/actions'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IWorkbenchLayoutService, Parts, Position, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; import { ActivityAction, ToggleCompositePinnedAction, ICompositeBar } from 'vs/workbench/browser/parts/compositeBarActions'; import { IActivity } from 'vs/workbench/common/activity'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { ActivePanelContext, PanelPositionContext } from 'vs/workbench/common/panel'; -import { ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; +import { ActivePanelContext, PanelMaximizedContext, PanelPositionContext, PanelVisibleContext } from 'vs/workbench/common/panel'; +import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { ViewContainerLocationToString, ViewContainerLocation } from 'vs/workbench/common/views'; const maximizeIcon = registerIcon('panel-maximize', Codicon.chevronUp, nls.localize('maximizeIcon', 'Icon to maximize a panel.')); const restoreIcon = registerIcon('panel-restore', Codicon.chevronDown, nls.localize('restoreIcon', 'Icon to restore a panel.')); const closeIcon = registerIcon('panel-close', Codicon.close, nls.localize('closeIcon', 'Icon to close a panel.')); -export class ClosePanelAction extends Action { - - static readonly ID = 'workbench.action.closePanel'; - static readonly LABEL = nls.localize('closePanel', "Close Panel"); - - constructor( - id: string, - name: string, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService - ) { - super(id, name, ThemeIcon.asClassName(closeIcon)); - } - - async run(): Promise { - this.layoutService.setPanelHidden(true); - } -} - export class TogglePanelAction extends Action { static readonly ID = 'workbench.action.togglePanel'; @@ -91,46 +72,6 @@ class FocusPanelAction extends Action { } } - -export class ToggleMaximizedPanelAction extends Action { - - static readonly ID = 'workbench.action.toggleMaximizedPanel'; - static readonly LABEL = nls.localize('toggleMaximizedPanel', "Toggle Maximized Panel"); - - private static readonly MAXIMIZE_LABEL = nls.localize('maximizePanel', "Maximize Panel Size"); - private static readonly RESTORE_LABEL = nls.localize('minimizePanel', "Restore Panel Size"); - - private readonly toDispose = this._register(new DisposableStore()); - - constructor( - id: string, - label: string, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IEditorGroupsService editorGroupsService: IEditorGroupsService - ) { - super(id, label, layoutService.isPanelMaximized() ? ThemeIcon.asClassName(restoreIcon) : ThemeIcon.asClassName(maximizeIcon)); - - this.toDispose.add(editorGroupsService.onDidLayout(() => { - const maximized = this.layoutService.isPanelMaximized(); - this.class = maximized ? ThemeIcon.asClassName(restoreIcon) : ThemeIcon.asClassName(maximizeIcon); - this.label = maximized ? ToggleMaximizedPanelAction.RESTORE_LABEL : ToggleMaximizedPanelAction.MAXIMIZE_LABEL; - })); - } - - async run(): Promise { - if (!this.layoutService.isVisible(Parts.PANEL_PART)) { - this.layoutService.setPanelHidden(false); - // If the panel is not already maximized, maximize it - if (!this.layoutService.isPanelMaximized()) { - this.layoutService.toggleMaximizedPanel(); - } - } - else { - this.layoutService.toggleMaximizedPanel(); - } - } -} - const PositionPanelActionId = { LEFT: 'workbench.action.positionPanelLeft', RIGHT: 'workbench.action.positionPanelRight', @@ -218,7 +159,6 @@ export class PlaceHolderToggleCompositePinnedAction extends ToggleCompositePinne } } - export class SwitchPanelViewAction extends Action { constructor( @@ -287,35 +227,117 @@ export class NextPanelViewAction extends SwitchPanelViewAction { const actionRegistry = Registry.as(WorkbenchExtensions.WorkbenchActions); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(TogglePanelAction, { primary: KeyMod.CtrlCmd | KeyCode.KEY_J }), 'View: Toggle Panel', CATEGORIES.View.value); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(FocusPanelAction), 'View: Focus into Panel', CATEGORIES.View.value); -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleMaximizedPanelAction), 'View: Toggle Maximized Panel', CATEGORIES.View.value); -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ClosePanelAction), 'View: Close Panel', CATEGORIES.View.value); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(PreviousPanelViewAction), 'View: Previous Panel View', CATEGORIES.View.value); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(NextPanelViewAction), 'View: Next Panel View', CATEGORIES.View.value); -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '2_workbench_layout', - command: { - id: TogglePanelAction.ID, - title: nls.localize({ key: 'miShowPanel', comment: ['&& denotes a mnemonic'] }, "Show &&Panel"), - toggled: ActivePanelContext - }, - order: 5 +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.toggleMaximizedPanel', + title: { value: nls.localize('toggleMaximizedPanel', "Toggle Maximized Panel"), original: 'Toggle Maximized Panel' }, + tooltip: nls.localize('maximizePanel', "Maximize Panel Size"), + category: CATEGORIES.View, + f1: true, + icon: maximizeIcon, + toggled: { condition: PanelMaximizedContext, icon: restoreIcon, tooltip: nls.localize('minimizePanel', "Restore Panel Size") }, + menu: [{ + id: MenuId.PanelTitle, + group: 'navigation', + order: 1 + }] + }); + } + run(accessor: ServicesAccessor) { + const layoutService = accessor.get(IWorkbenchLayoutService); + if (!layoutService.isVisible(Parts.PANEL_PART)) { + layoutService.setPanelHidden(false); + // If the panel is not already maximized, maximize it + if (!layoutService.isPanelMaximized()) { + layoutService.toggleMaximizedPanel(); + } + } + else { + layoutService.toggleMaximizedPanel(); + } + } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.closePanel', + title: { value: nls.localize('closePanel', "Close Panel"), original: 'Close Panel' }, + category: CATEGORIES.View, + icon: closeIcon, + menu: [{ + id: MenuId.CommandPalette, + when: PanelVisibleContext, + }, { + id: MenuId.PanelTitle, + group: 'navigation', + order: 2 + }] + }); + } + run(accessor: ServicesAccessor) { + accessor.get(IWorkbenchLayoutService).setPanelHidden(true); + } +}); + +MenuRegistry.appendMenuItems([ + { + id: MenuId.MenubarAppearanceMenu, + item: { + group: '2_workbench_layout', + command: { + id: TogglePanelAction.ID, + title: nls.localize({ key: 'miShowPanel', comment: ['&& denotes a mnemonic'] }, "Show &&Panel"), + toggled: ActivePanelContext + }, + order: 5 + } + }, { + id: MenuId.ViewTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: TogglePanelAction.ID, + title: { value: nls.localize('hidePanel', "Hide Panel"), original: 'Hide Panel' }, + }, + when: ContextKeyExpr.and(PanelVisibleContext, ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Panel))), + order: 2 + } + } +]); + function registerPositionPanelActionById(config: PanelActionConfig) { const { id, label, alias, when } = config; // register the workbench action actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(SetPanelPositionAction, id, label), alias, CATEGORIES.View.value, when); // register as a menu item - MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '3_workbench_layout_move', - command: { - id, - title: label - }, - when, - order: 5 - }); + MenuRegistry.appendMenuItems([{ + id: MenuId.MenubarAppearanceMenu, + item: { + group: '3_workbench_layout_move', + command: { + id, + title: label + }, + when, + order: 5 + } + }, { + id: MenuId.ViewTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: id, + title: label, + }, + when: ContextKeyExpr.and(when, ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Panel))), + order: 1 + } + }]); } // register each position panel action diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 332a9ab4..b4727ebc 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/panelpart'; -import { IAction, Action } from 'vs/base/common/actions'; +import { localize } from 'vs/nls'; +import { IAction, Separator, toAction } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -18,8 +19,8 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ClosePanelAction, PanelActivityAction, ToggleMaximizedPanelAction, TogglePanelAction, PlaceHolderPanelActivityAction, PlaceHolderToggleCompositePinnedAction, PositionPanelActionConfigs, SetPanelPositionAction } from 'vs/workbench/browser/parts/panel/panelActions'; -import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; +import { PanelActivityAction, TogglePanelAction, PlaceHolderPanelActivityAction, PlaceHolderToggleCompositePinnedAction, PositionPanelActionConfigs, SetPanelPositionAction } from 'vs/workbench/browser/parts/panel/panelActions'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { PANEL_BACKGROUND, PANEL_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_INPUT_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, PANEL_DRAG_AND_DROP_BORDER } from 'vs/workbench/common/theme'; import { activeContrastBorder, focusBorder, contrastBorder, editorBackground, badgeBackground, badgeForeground } from 'vs/platform/theme/common/colorRegistry'; import { CompositeBar, ICompositeBarItem, CompositeDragAndDrop } from 'vs/workbench/browser/parts/compositeBar'; @@ -27,15 +28,12 @@ import { ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/composit import { IBadge } from 'vs/workbench/services/activity/common/activity'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { Dimension, trackFocus, EventHelper } from 'vs/base/browser/dom'; -import { localize } from 'vs/nls'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; 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, IViewDescriptorService, IViewContainerModel, ViewContainerLocation } from 'vs/workbench/common/views'; -import { MenuId } from 'vs/platform/actions/common/actions'; -import { ViewMenuActions, ViewContainerMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; import { Before2D, CompositeDragAndDropObserver, ICompositeDragAndDrop, toggleDropEffect } from 'vs/workbench/browser/dnd'; import { IActivity } from 'vs/workbench/common/activity'; @@ -46,7 +44,7 @@ interface ICachedPanel { pinned: boolean; order?: number; visible: boolean; - views?: { when?: string }[]; + views?: { when?: string; }[]; } interface IPlaceholderViewContainer { @@ -148,24 +146,27 @@ export class PanelPart extends CompositePart implements IPanelService { this.compositeBar = this._register(this.instantiationService.createInstance(CompositeBar, this.getCachedPanels(), { icon: false, orientation: ActionsOrientation.HORIZONTAL, - openComposite: (compositeId: string) => this.openPanel(compositeId, true).then(panel => panel || null), - getActivityAction: (compositeId: string) => this.getCompositeActions(compositeId).activityAction, - getCompositePinnedAction: (compositeId: string) => this.getCompositeActions(compositeId).pinnedAction, - getOnCompositeClickAction: (compositeId: string) => this.instantiationService.createInstance(PanelActivityAction, assertIsDefined(this.getPanel(compositeId))), - getContextMenuActions: () => [ - ...PositionPanelActionConfigs - // show the contextual menu item if it is not in that position - .filter(({ when }) => contextKeyService.contextMatchesRules(when)) - .map(({ id, label }) => this.instantiationService.createInstance(SetPanelPositionAction, id, label)), - this.instantiationService.createInstance(TogglePanelAction, TogglePanelAction.ID, localize('hidePanel', "Hide Panel")) - ] as Action[], - getContextMenuActionsForComposite: (compositeId: string) => this.getContextMenuActionsForComposite(compositeId) as Action[], + openComposite: compositeId => this.openPanel(compositeId, true).then(panel => panel || null), + getActivityAction: compositeId => this.getCompositeActions(compositeId).activityAction, + getCompositePinnedAction: compositeId => this.getCompositeActions(compositeId).pinnedAction, + getOnCompositeClickAction: compositeId => this.instantiationService.createInstance(PanelActivityAction, assertIsDefined(this.getPanel(compositeId))), + fillExtraContextMenuActions: actions => { + actions.push(...[ + new Separator(), + ...PositionPanelActionConfigs + // show the contextual menu item if it is not in that position + .filter(({ when }) => contextKeyService.contextMatchesRules(when)) + .map(({ id, label }) => this.instantiationService.createInstance(SetPanelPositionAction, id, label)), + this.instantiationService.createInstance(TogglePanelAction, TogglePanelAction.ID, localize('hidePanel', "Hide Panel")) + ]); + }, + getContextMenuActionsForComposite: compositeId => this.getContextMenuActionsForComposite(compositeId), getDefaultCompositeId: () => this.panelRegistry.getDefaultPanelId(), hidePart: () => this.layoutService.setPanelHidden(true), dndHandler: this.dndHandler, compositeSize: 0, overflowActionSize: 44, - colors: (theme: IColorTheme) => ({ + colors: theme => ({ activeBackgroundColor: theme.getColor(PANEL_BACKGROUND), // Background color for overflow action inactiveBackgroundColor: theme.getColor(PANEL_BACKGROUND), // Background color for overflow action activeBorderBottomColor: theme.getColor(PANEL_ACTIVE_TITLE_BORDER), @@ -184,20 +185,21 @@ export class PanelPart extends CompositePart implements IPanelService { this.onDidRegisterPanels([...this.getPanels()]); } - private getContextMenuActionsForComposite(compositeId: string): readonly IAction[] { + private getContextMenuActionsForComposite(compositeId: string): IAction[] { const result: IAction[] = []; - const container = this.getViewContainer(compositeId); - if (container) { - const viewContainerModel = this.viewDescriptorService.getViewContainerModel(container); + const viewContainer = this.viewDescriptorService.getViewContainerById(compositeId)!; + const defaultLocation = this.viewDescriptorService.getDefaultViewContainerLocation(viewContainer)!; + if (defaultLocation !== this.viewDescriptorService.getViewContainerLocation(viewContainer)) { + result.push(toAction({ id: 'resetLocationAction', label: localize('resetLocation', "Reset Location"), run: () => this.viewDescriptorService.moveViewContainerToLocation(viewContainer, defaultLocation) })); + } else { + const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); if (viewContainerModel.allViewDescriptors.length === 1) { - const viewMenuActions = this.instantiationService.createInstance(ViewMenuActions, viewContainerModel.allViewDescriptors[0].id, MenuId.ViewTitle, MenuId.ViewTitleContext); - result.push(...viewMenuActions.getContextMenuActions()); - viewMenuActions.dispose(); + const viewToReset = viewContainerModel.allViewDescriptors[0]; + const defaultContainer = this.viewDescriptorService.getDefaultContainerById(viewToReset.id)!; + if (defaultContainer !== viewContainer) { + result.push(toAction({ id: 'resetLocationAction', label: localize('resetLocation', "Reset Location"), run: () => this.viewDescriptorService.moveViewsToContainer([viewToReset], defaultContainer) })); + } } - - const viewContainerMenuActions = this.instantiationService.createInstance(ViewContainerMenuActions, container.id, MenuId.ViewContainerTitleContext); - result.push(...viewContainerMenuActions.getContextMenuActions()); - viewContainerMenuActions.dispose(); } return result; } @@ -534,13 +536,6 @@ export class PanelPart extends CompositePart implements IPanelService { .sort((p1, p2) => pinnedCompositeIds.indexOf(p1.id) - pinnedCompositeIds.indexOf(p2.id)); } - protected getActions(): ReadonlyArray { - return [ - this.instantiationService.createInstance(ToggleMaximizedPanelAction, ToggleMaximizedPanelAction.ID, ToggleMaximizedPanelAction.LABEL), - this.instantiationService.createInstance(ClosePanelAction, ClosePanelAction.ID, ClosePanelAction.LABEL) - ]; - } - getActivePanel(): IPanel | undefined { return this.getActiveComposite(); } @@ -803,7 +798,7 @@ export class PanelPart extends CompositePart implements IPanelService { } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { // Panel Background: since panels can host editors, we apply a background rule if the panel background // color is different from the editor background color. This is a bit of a hack though. The better way diff --git a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css index c5953e53..c581f528 100644 --- a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css +++ b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css @@ -3,11 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* Removed to allow progress bar positioning to escape */ -/* .monaco-workbench .sidebar > .content { - overflow: hidden; -} */ - .monaco-workbench.nosidebar > .part.sidebar { display: none !important; visibility: hidden !important; diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index f6504de9..671a0582 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -6,7 +6,6 @@ import 'vs/css!./media/sidebarpart'; import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -import { Action } from 'vs/base/common/actions'; import { CompositePart } from 'vs/workbench/browser/parts/compositePart'; import { Viewlet, ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor } from 'vs/workbench/browser/viewlet'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; @@ -288,8 +287,8 @@ export class SidebarPart extends CompositePart implements IViewletServi const anchor: { x: number, y: number } = { x: event.posx, y: event.posy }; this.contextMenuService.showContextMenu({ getAnchor: () => anchor, - getActions: () => contextMenuActions, - getActionViewItem: action => this.actionViewItemProvider(action as Action), + getActions: () => contextMenuActions.slice(), + getActionViewItem: action => this.actionViewItemProvider(action), actionRunner: activeViewlet.getActionRunner() }); } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 75903969..aa9d9f05 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/statusbarpart'; import * as nls from 'vs/nls'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { dispose, IDisposable, Disposable, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel'; +import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { Part } from 'vs/workbench/browser/part'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -15,13 +15,12 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { StatusbarAlignment, IStatusbarService, IStatusbarEntry, IStatusbarEntryAccessor } from 'vs/workbench/services/statusbar/common/statusbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { Action, IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, Separator } from 'vs/base/common/actions'; -import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector, ThemeColor } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant, ThemeColor } from 'vs/platform/theme/common/themeService'; import { STATUS_BAR_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_ITEM_ACTIVE_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND, STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND, STATUS_BAR_BORDER, STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_NO_FOLDER_BORDER } from 'vs/workbench/common/theme'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { isThemeColor } from 'vs/editor/common/editorCommon'; -import { Color } from 'vs/base/common/color'; -import { EventHelper, createStyleSheet, addDisposableListener, EventType, hide, show, isAncestor, appendChildren } from 'vs/base/browser/dom'; +import { EventHelper, createStyleSheet, addDisposableListener, EventType, hide, show, isAncestor, append } from 'vs/base/browser/dom'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, IStorageValueChangeEvent, StorageTarget } from 'vs/platform/storage/common/storage'; import { Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; @@ -38,7 +37,8 @@ import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/co import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ColorScheme } from 'vs/platform/theme/common/theme'; -import { renderCodicon, renderCodicons } from 'vs/base/browser/codicons'; +import { renderIcon, renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { syncing } from 'vs/platform/theme/common/iconRegistry'; interface IPendingStatusbarEntry { id: string; @@ -610,7 +610,7 @@ export class StatusbarPart extends Part implements IStatusbarService { } private getContextMenuActions(event: StandardMouseEvent): IAction[] { - const actions: Action[] = []; + const actions: IAction[] = []; // Provide an action to hide the status bar at last actions.push(this.instantiationService.createInstance(ToggleStatusbarVisibilityAction, ToggleStatusbarVisibilityAction.ID, nls.localize('hideStatusBar', "Hide Status Bar"))); @@ -704,9 +704,9 @@ export class StatusbarPart extends Part implements IStatusbarService { } } -class StatusBarCodiconLabel extends CodiconLabel { +class StatusBarCodiconLabel extends SimpleIconLabel { - private readonly progressCodicon = renderCodicon('sync', 'spin'); + private readonly progressCodicon = renderIcon(syncing); private currentText = ''; private currentShowProgress = false; @@ -749,7 +749,7 @@ class StatusBarCodiconLabel extends CodiconLabel { } // Append new elements - appendChildren(this.container, ...renderCodicons(textContent)); + append(this.container, ...renderLabelWithIcons(textContent)); } // No Progress: no special handling @@ -868,12 +868,8 @@ class StatusbarEntryItem extends Disposable { // Update: Background if (!this.entry || entry.backgroundColor !== this.entry.backgroundColor) { - if (entry.backgroundColor) { - this.applyColor(this.container, entry.backgroundColor, true); - this.container.classList.add('has-background-color'); - } else { - this.container.classList.remove('has-background-color'); - } + this.container.classList.toggle('has-background-color', !!entry.backgroundColor); + this.applyColor(this.container, entry.backgroundColor, true); } // Remember for next round @@ -893,7 +889,7 @@ class StatusbarEntryItem extends Disposable { } private applyColor(container: HTMLElement, color: string | ThemeColor | undefined, isBackground?: boolean): void { - let colorResult: string | null = null; + let colorResult: string | undefined = undefined; if (isBackground) { this.backgroundListener.clear(); @@ -903,15 +899,15 @@ class StatusbarEntryItem extends Disposable { if (color) { if (isThemeColor(color)) { - colorResult = (this.themeService.getColorTheme().getColor(color.id) || Color.transparent).toString(); + colorResult = this.themeService.getColorTheme().getColor(color.id)?.toString(); const listener = this.themeService.onDidColorThemeChange(theme => { - const colorValue = (theme.getColor(color.id) || Color.transparent).toString(); + const colorValue = theme.getColor(color.id)?.toString(); if (isBackground) { - container.style.backgroundColor = colorValue; + container.style.backgroundColor = colorValue ?? ''; } else { - container.style.color = colorValue; + container.style.color = colorValue ?? ''; } }); @@ -926,9 +922,9 @@ class StatusbarEntryItem extends Disposable { } if (isBackground) { - container.style.backgroundColor = colorResult || ''; + container.style.backgroundColor = colorResult ?? ''; } else { - container.style.color = colorResult || ''; + container.style.color = colorResult ?? ''; } } @@ -942,7 +938,7 @@ class StatusbarEntryItem extends Disposable { } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { if (theme.type !== ColorScheme.HIGH_CONTRAST) { const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND); if (statusBarItemHoverBackground) { diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index a4c34e45..7ac774cb 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -85,11 +85,34 @@ height: 100%; position: relative; z-index: 3000; + flex-shrink: 0; +} + +.monaco-workbench .part.titlebar > .window-appicon:not(.codicon) { background-image: url('../../../media/code-icon.svg'); background-repeat: no-repeat; background-position: center center; background-size: 16px; - flex-shrink: 0; +} + +.monaco-workbench .part.titlebar .window-appicon > .home-bar-icon-badge { + position: absolute; + right: 9px; + bottom: 6px; + width: 8px; + height: 8px; + z-index: 1; /* on top of home indicator */ + background-image: url('../../../media/code-icon.svg'); + background-repeat: no-repeat; + background-position: center center; + background-size: 8px; + pointer-events: none; + border-top: 1px solid transparent; + border-left: 1px solid transparent; +} + +.monaco-workbench .part.titlebar > .window-appicon.codicon { + line-height: 30px; } .monaco-workbench.fullscreen .part.titlebar > .window-appicon { diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index 99fcf2b6..886c2ff6 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { IMenuService, MenuId, IMenu, SubmenuItemAction, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; -import { registerThemingParticipant, IColorTheme, ICssStyleCollector, IThemeService } from 'vs/platform/theme/common/themeService'; +import { IMenuService, MenuId, IMenu, SubmenuItemAction, registerAction2, Action2, MenuRegistry, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { registerThemingParticipant, IThemeService } from 'vs/platform/theme/common/themeService'; import { MenuBarVisibility, getTitleBarStyle, IWindowOpenable, getMenuBarVisibility } from 'vs/platform/windows/common/windows'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IAction, Action, SubmenuAction, Separator } from 'vs/base/common/actions'; import * as DOM from 'vs/base/browser/dom'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -37,6 +37,7 @@ import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { KeyCode } from 'vs/base/common/keyCodes'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; +import { ICommandService } from 'vs/platform/commands/common/commands'; export abstract class MenubarControl extends Disposable { @@ -91,7 +92,8 @@ export abstract class MenubarControl extends Disposable { protected readonly preferencesService: IPreferencesService, protected readonly environmentService: IWorkbenchEnvironmentService, protected readonly accessibilityService: IAccessibilityService, - protected readonly hostService: IHostService + protected readonly hostService: IHostService, + protected readonly commandService: ICommandService ) { super(); @@ -308,23 +310,10 @@ export class CustomMenubarControl extends MenubarControl { @IAccessibilityService accessibilityService: IAccessibilityService, @IThemeService private readonly themeService: IThemeService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IHostService protected readonly hostService: IHostService + @IHostService protected readonly hostService: IHostService, + @ICommandService commandService: ICommandService ) { - super( - menuService, - workspacesService, - contextKeyService, - keybindingService, - configurationService, - labelService, - updateService, - storageService, - notificationService, - preferencesService, - environmentService, - accessibilityService, - hostService - ); + super(menuService, workspacesService, contextKeyService, keybindingService, configurationService, labelService, updateService, storageService, notificationService, preferencesService, environmentService, accessibilityService, hostService, commandService); this._onVisibilityChange = this._register(new Emitter()); this._onFocusStateChange = this._register(new Emitter()); @@ -337,7 +326,17 @@ export class CustomMenubarControl extends MenubarControl { this.registerActions(); - registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { + // Register web menu actions to the file menu when its in the title + this.getWebNavigationMenuItemActions().forEach(actionItem => { + this._register(MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + command: actionItem.item, + title: actionItem.item.title, + group: 'z_Web', + when: ContextKeyExpr.and(IsWebContext, ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'compact')) + })); + }); + + registerThemingParticipant((theme, collector) => { const menubarActiveWindowFgColor = theme.getColor(TITLE_BAR_ACTIVE_FOREGROUND); if (menubarActiveWindowFgColor) { collector.addRule(` @@ -356,7 +355,6 @@ export class CustomMenubarControl extends MenubarControl { color: ${activityBarInactiveFgColor}; } `); - } const activityBarFgColor = theme.getColor(ACTIVITY_BAR_FOREGROUND); @@ -383,7 +381,6 @@ export class CustomMenubarControl extends MenubarControl { `); } - const menubarSelectedFgColor = theme.getColor(MENUBAR_SELECTION_FOREGROUND); if (menubarSelectedFgColor) { collector.addRule(` @@ -608,6 +605,12 @@ export class CustomMenubarControl extends MenubarControl { for (let action of actions) { this.insertActionsBefore(action, target); + + // use mnemonicTitle whenever possible + const title = typeof action.item.title === 'string' + ? action.item.title + : action.item.title.mnemonicTitle ?? action.item.title.value; + if (action instanceof SubmenuItemAction) { let submenu = this.menus[action.item.submenu.id]; if (!submenu) { @@ -625,10 +628,12 @@ export class CustomMenubarControl extends MenubarControl { const submenuActions: SubmenuAction[] = []; updateActions(submenu, submenuActions, topLevelTitle); - target.push(new SubmenuAction(action.id, mnemonicMenuLabel(action.label), submenuActions)); + target.push(new SubmenuAction(action.id, mnemonicMenuLabel(title), submenuActions)); } else { - action.label = mnemonicMenuLabel(this.calculateActionLabel(action)); - target.push(action); + const newAction = new Action(action.id, mnemonicMenuLabel(title), action.class, action.enabled, () => this.commandService.executeCommand(action.id)); + newAction.tooltip = action.tooltip; + newAction.checked = action.checked; + target.push(newAction); } } @@ -667,6 +672,27 @@ export class CustomMenubarControl extends MenubarControl { } } + private getWebNavigationMenuItemActions(): MenuItemAction[] { + if (!isWeb) { + return []; // only for web + } + + const webNavigationActions = []; + const webNavigationMenu = this.menuService.createMenu(MenuId.MenubarHomeMenu, this.contextKeyService); + for (const groups of webNavigationMenu.getActions()) { + const [, actions] = groups; + for (const action of actions) { + if (action instanceof MenuItemAction) { + webNavigationActions.push(action); + } + } + } + + webNavigationMenu.dispose(); + + return webNavigationActions; + } + private getMenuBarOptions(): IMenuBarOptions { return { enableMnemonics: this.currentEnableMenuBarMnemonics, @@ -674,7 +700,35 @@ export class CustomMenubarControl extends MenubarControl { visibility: this.currentMenubarVisibility, getKeybinding: (action) => this.keybindingService.lookupKeybinding(action.id), alwaysOnMnemonics: this.alwaysOnMnemonics, - compactMode: this.currentCompactMenuMode + compactMode: this.currentCompactMenuMode, + getCompactMenuActions: () => { + if (!isWeb) { + return []; // only for web + } + + const webNavigationActions: IAction[] = []; + const href = this.environmentService.options?.homeIndicator?.href; + if (href) { + webNavigationActions.push(new Action('goHome', nls.localize('goHome', "Go Home"), undefined, true, + async (event?: MouseEvent) => { + if ((!isMacintosh && event?.ctrlKey) || (isMacintosh && event?.metaKey)) { + window.open(href, '_blank'); + } else { + window.location.href = href; + } + })); + } + + const otherActions = this.getWebNavigationMenuItemActions().map(action => { + const title = typeof action.item.title === 'string' + ? action.item.title + : action.item.title.mnemonicTitle ?? action.item.title.value; + return new Action(action.id, mnemonicMenuLabel(title), action.class, action.enabled, () => this.commandService.executeCommand(action.id)); + }); + + webNavigationActions.push(...otherActions); + return webNavigationActions; + } }; } diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 4783cb52..52a1f2f4 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/titlebarpart'; +import { localize } from 'vs/nls'; import { dirname, basename } from 'vs/base/common/resources'; import { Part } from 'vs/workbench/browser/part'; import { ITitleService, ITitleProperties } from 'vs/workbench/services/title/common/titleService'; @@ -15,17 +16,16 @@ import { IAction } from 'vs/base/common/actions'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; -import * as nls from 'vs/nls'; import { EditorResourceAccessor, Verbosity, SideBySideEditor } from 'vs/workbench/common/editor'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { TITLE_BAR_ACTIVE_BACKGROUND, TITLE_BAR_ACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_BACKGROUND, TITLE_BAR_BORDER, WORKBENCH_BACKGROUND } from 'vs/workbench/common/theme'; import { isMacintosh, isWindows, isLinux, isWeb } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { Color } from 'vs/base/common/color'; import { trim } from 'vs/base/common/strings'; -import { EventType, EventHelper, Dimension, isAncestor, append, $, addDisposableListener, runAtThisOrScheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; +import { EventType, EventHelper, Dimension, isAncestor, append, $, addDisposableListener, runAtThisOrScheduleAtNextAnimationFrame, prepend } from 'vs/base/browser/dom'; import { CustomMenubarControl } from 'vs/workbench/browser/parts/titlebar/menubarControl'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { template } from 'vs/base/common/labels'; @@ -41,12 +41,13 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IProductService } from 'vs/platform/product/common/productService'; import { Schemas } from 'vs/base/common/network'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { Codicon, iconRegistry } from 'vs/base/common/codicons'; export class TitlebarPart extends Part implements ITitleService { - private static readonly NLS_UNSUPPORTED = nls.localize('patchedWindowTitle', "[Unsupported]"); - private static readonly NLS_USER_IS_ADMIN = isWindows ? nls.localize('userIsAdmin', "[Administrator]") : nls.localize('userIsSudo', "[Superuser]"); - private static readonly NLS_EXTENSION_HOST = nls.localize('devExtensionWindowTitlePrefix', "[Extension Development Host]"); + private static readonly NLS_UNSUPPORTED = localize('patchedWindowTitle', "[Unsupported]"); + private static readonly NLS_USER_IS_ADMIN = isWindows ? localize('userIsAdmin', "[Administrator]") : localize('userIsSudo', "[Superuser]"); + private static readonly NLS_EXTENSION_HOST = localize('devExtensionWindowTitlePrefix', "[Extension Development Host]"); private static readonly TITLE_DIRTY = '\u25cf '; //#region IView @@ -65,6 +66,8 @@ export class TitlebarPart extends Part implements ITitleService { protected title!: HTMLElement; protected customMenubar: CustomMenubarControl | undefined; + protected appIcon: HTMLElement | undefined; + private appIconBadge: HTMLElement | undefined; protected menubar?: HTMLElement; protected lastLayoutDimensions: Dimension | undefined; private titleBarStyle: 'native' | 'custom'; @@ -86,7 +89,7 @@ export class TitlebarPart extends Part implements ITitleService { @IEditorService private readonly editorService: IEditorService, @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @ILabelService private readonly labelService: ILabelService, @IStorageService storageService: IStorageService, @@ -341,6 +344,28 @@ export class TitlebarPart extends Part implements ITitleService { createContentArea(parent: HTMLElement): HTMLElement { this.element = parent; + // App Icon (Native Windows/Linux and Web) + if (!isMacintosh || isWeb) { + this.appIcon = prepend(this.element, $('a.window-appicon')); + + // Web-only home indicator and menu + if (isWeb) { + const homeIndicator = this.environmentService.options?.homeIndicator; + if (homeIndicator) { + let codicon = iconRegistry.get(homeIndicator.icon); + if (!codicon) { + codicon = Codicon.code; + } + + this.appIcon.setAttribute('href', homeIndicator.href); + this.appIcon.classList.add(...codicon.classNamesArray); + this.appIconBadge = document.createElement('div'); + this.appIconBadge.classList.add('home-bar-icon-badge'); + this.appIcon.appendChild(this.appIconBadge); + } + } + } + // Menubar: install a custom menu bar depending on configuration // and when not in activity bar if (this.titleBarStyle !== 'native' @@ -407,6 +432,11 @@ export class TitlebarPart extends Part implements ITitleService { return color.isOpaque() ? color : color.makeOpaque(WORKBENCH_BACKGROUND(theme)); }) || ''; this.element.style.backgroundColor = titleBackground; + + if (this.appIconBadge) { + this.appIconBadge.style.backgroundColor = titleBackground; + } + if (titleBackground && Color.fromHex(titleBackground).isLighter()) { this.element.classList.add('light'); } else { @@ -441,7 +471,7 @@ export class TitlebarPart extends Part implements ITitleService { protected adjustTitleMarginToCenter(): void { if (this.customMenubar && this.menubar) { - const leftMarker = this.menubar.clientWidth + 10; + const leftMarker = (this.appIcon ? this.appIcon.clientWidth : 0) + this.menubar.clientWidth + 10; const rightMarker = this.element.clientWidth - 10; // Not enough space to center the titlebar within window, @@ -497,7 +527,7 @@ export class TitlebarPart extends Part implements ITitleService { } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { const titlebarActiveFg = theme.getColor(TITLE_BAR_ACTIVE_FOREGROUND); if (titlebarActiveFg) { collector.addRule(` diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index dbd8ffce..73c7e5e4 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -8,19 +8,19 @@ import { toDisposable, IDisposable, Disposable, DisposableStore } from 'vs/base/ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { MenuId, IMenuService, MenuItemAction, registerAction2, Action2, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { MenuId, IMenuService, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IContextKeyService, ContextKeyExpr, ContextKeyEqualsExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ITreeView, ITreeViewDescriptor, IViewsRegistry, Extensions, IViewDescriptorService, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, ViewContainer, ViewContainerLocation, ResolvableTreeItem } from 'vs/workbench/common/views'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IThemeService, FileThemeIcon, FolderThemeIcon, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { Registry } from 'vs/platform/registry/common/platform'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Event, Emitter } from 'vs/base/common/event'; import { IAction, ActionRunner, IActionViewItemProvider } from 'vs/base/common/actions'; -import { MenuEntryActionViewItem, createAndFillInContextMenuActions, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { createAndFillInContextMenuActions, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -52,6 +52,8 @@ import { IIconLabelMarkdownString } from 'vs/base/browser/ui/iconLabel/iconLabel import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; import { API_OPEN_DIFF_EDITOR_COMMAND_ID, API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { Codicon } from 'vs/base/common/codicons'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Command } from 'vs/editor/common/modes'; export class TreeViewPane extends ViewPane { @@ -453,15 +455,7 @@ export class TreeView extends Disposable implements ITreeView { } private createTree() { - const actionViewItemProvider = (action: IAction) => { - if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action); - } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); - } - - return undefined; - }; + const actionViewItemProvider = createActionViewItem.bind(undefined, this.instantiationService); const treeMenus = this._register(this.instantiationService.createInstance(TreeMenus, this.id)); this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); const dataSource = this.instantiationService.createInstance(TreeDataSource, this, (task: Promise) => this.progressService.withProgress({ location: this.id }, () => task)); @@ -534,12 +528,13 @@ export class TreeView extends Disposable implements ITreeView { })); this.tree.setInput(this.root).then(() => this.updateContentAreas()); - this._register(this.tree.onDidOpen(e => { + this._register(this.tree.onDidOpen(async (e) => { if (!e.browserEvent) { return; } const selection = this.tree!.getSelection(); - const command = selection.length === 1 ? selection[0].command : undefined; + const command = await this.resolveCommand(selection.length === 1 ? selection[0] : undefined); + if (command) { let args = command.arguments || []; if (command.id === API_OPEN_EDITOR_COMMAND_ID || command.id === API_OPEN_DIFF_EDITOR_COMMAND_ID) { @@ -554,6 +549,17 @@ export class TreeView extends Disposable implements ITreeView { } + private async resolveCommand(element: ITreeItem | undefined): Promise { + let command = element?.command; + if (element && !command) { + if ((element instanceof ResolvableTreeItem) && element.hasResolve) { + await element.resolve(new CancellationTokenSource().token); + command = element.command; + } + } + return command; + } + private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent, actionRunner: MultipleSelectionActionRunner): void { this.hoverService.hideHover(); const node: ITreeItem | null = treeEvent.element; @@ -776,10 +782,17 @@ class TreeDataSource implements IAsyncDataSource { } async getChildren(element: ITreeItem): Promise { + let result: ITreeItem[] = []; if (this.treeView.dataProvider) { - return this.withProgress(this.treeView.dataProvider.getChildren(element)); + try { + result = await this.withProgress(this.treeView.dataProvider.getChildren(element)); + } catch (e) { + if (!(e.message).startsWith('Bad progress location:')) { + throw e; + } + } } - return []; + return result; } } @@ -840,6 +853,9 @@ class TreeRenderer extends Disposable implements ITreeRenderer { return this.hoverService.showHover(options); + }, + hideHover: () => { + return this.hoverService.hideHover(); } }; } @@ -868,7 +884,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer => { + markdown: (token: CancellationToken): Promise => { return new Promise(async (resolve) => { - await node.resolve(); + await node.resolve(token); resolve(node.tooltip); }); }, - markdownNotSupportedFallback: resource ? undefined : '' // Passing undefined as the fallback for a resource falls back to the old native hover + markdownNotSupportedFallback: resource ? undefined : label ?? '' // Passing undefined as the fallback for a resource falls back to the old native hover }; } @@ -928,7 +944,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer()); - readonly onDidChangeTitle: Event = this._onDidChangeTitle.event; - - constructor( - viewId: string, - menuId: MenuId, - contextMenuId: 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, { shouldForwardArgs: true }, { primary: this.primaryActions, secondary: this.secondaryActions }); - this._onDidChangeTitle.fire(); - }; - this._register(menu.onDidChange(updateActions)); - updateActions(); - - const contextMenu = this._register(this.menuService.createMenu(contextMenuId, scopedContextKeyService)); - const updateContextMenuActions = () => { - this.contextMenuActions = []; - this.titleActionsDisposable.value = createAndFillInActionBarActions(contextMenu, { shouldForwardArgs: true }, { primary: [], secondary: this.contextMenuActions }); - }; - this._register(contextMenu.onDidChange(updateContextMenuActions)); - updateContextMenuActions(); - - this._register(toDisposable(() => { - this.primaryActions = []; - this.secondaryActions = []; - this.contextMenuActions = []; - })); - } - - getPrimaryActions(): IAction[] { - return this.primaryActions; - } - - getSecondaryActions(): IAction[] { - return this.secondaryActions; - } - - getContextMenuActions(): IAction[] { - return this.contextMenuActions; - } -} - -export class ViewContainerMenuActions extends Disposable { - - private readonly titleActionsDisposable = this._register(new MutableDisposable()); - private contextMenuActions: IAction[] = []; - - constructor( - containerId: string, - contextMenuId: MenuId, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IMenuService private readonly menuService: IMenuService, - ) { - super(); - - const scopedContextKeyService = this._register(this.contextKeyService.createScoped()); - scopedContextKeyService.createKey('container', containerId); - - const contextMenu = this._register(this.menuService.createMenu(contextMenuId, scopedContextKeyService)); - const updateContextMenuActions = () => { - this.contextMenuActions = []; - this.titleActionsDisposable.value = createAndFillInActionBarActions(contextMenu, { shouldForwardArgs: true }, { primary: [], secondary: this.contextMenuActions }); - }; - this._register(contextMenu.onDidChange(updateContextMenuActions)); - updateContextMenuActions(); - - this._register(toDisposable(() => { - this.contextMenuActions = []; - })); - } - - getContextMenuActions(): IAction[] { - return this.contextMenuActions; - } -} diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts new file mode 100644 index 00000000..27f5af3c --- /dev/null +++ b/src/vs/workbench/browser/parts/views/viewPane.ts @@ -0,0 +1,642 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/paneviewlet'; +import * as nls from 'vs/nls'; +import { Event, Emitter } from 'vs/base/common/event'; +import { foreground } from 'vs/platform/theme/common/colorRegistry'; +import { attachButtonStyler, attachLinkStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler'; +import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { after, append, $, trackFocus, EventType, addDisposableListener, createCSSRule, asCSSUrl } from 'vs/base/browser/dom'; +import { IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IAction, IActionViewItem } from 'vs/base/common/actions'; +import { ActionsOrientation, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IPaneOptions, Pane, IPaneStyles } from 'vs/base/browser/ui/splitview/paneview'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Extensions as ViewContainerExtensions, IView, FocusedViewContext, IViewDescriptorService, ViewContainerLocation, IViewsRegistry, IViewContentDescriptor, defaultViewIcon, IViewsService, ViewContainerLocationToString } from 'vs/workbench/common/views'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { assertIsDefined } from 'vs/base/common/types'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { MenuId, Action2, IAction2Options, IMenuService } from 'vs/platform/actions/common/actions'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { parseLinkedText } from 'vs/base/common/linkedText'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { Link } from 'vs/platform/opener/browser/link'; +import { Orientation } from 'vs/base/browser/ui/sash/sash'; +import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; +import { CompositeProgressIndicator } from 'vs/workbench/services/progress/browser/progressIndicator'; +import { IProgressIndicator } from 'vs/platform/progress/common/progress'; +import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { URI } from 'vs/base/common/uri'; +import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { Codicon } from 'vs/base/common/codicons'; +import { CompositeMenuActions } from 'vs/workbench/browser/menuActions'; + +export interface IViewPaneOptions extends IPaneOptions { + id: string; + showActionsAlways?: boolean; + titleMenuId?: MenuId; +} + +type WelcomeActionClassification = { + viewId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + uri: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; +}; + +const viewPaneContainerExpandedIcon = registerIcon('view-pane-container-expanded', Codicon.chevronDown, nls.localize('viewPaneContainerExpandedIcon', 'Icon for an expanded view pane container.')); +const viewPaneContainerCollapsedIcon = registerIcon('view-pane-container-collapsed', Codicon.chevronRight, nls.localize('viewPaneContainerCollapsedIcon', 'Icon for a collapsed view pane container.')); + +const viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); + +interface IItem { + readonly descriptor: IViewContentDescriptor; + visible: boolean; +} + +class ViewWelcomeController { + + private _onDidChange = new Emitter(); + readonly onDidChange = this._onDidChange.event; + + private defaultItem: IItem | undefined; + private items: IItem[] = []; + get contents(): IViewContentDescriptor[] { + const visibleItems = this.items.filter(v => v.visible); + + if (visibleItems.length === 0 && this.defaultItem) { + return [this.defaultItem.descriptor]; + } + + return visibleItems.map(v => v.descriptor); + } + + private contextKeyService: IContextKeyService; + private disposables = new DisposableStore(); + + constructor( + private id: string, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + this.contextKeyService = contextKeyService.createScoped(); + this.disposables.add(this.contextKeyService); + + contextKeyService.onDidChangeContext(this.onDidChangeContext, this, this.disposables); + Event.filter(viewsRegistry.onDidChangeViewWelcomeContent, id => id === this.id)(this.onDidChangeViewWelcomeContent, this, this.disposables); + this.onDidChangeViewWelcomeContent(); + } + + private onDidChangeViewWelcomeContent(): void { + const descriptors = viewsRegistry.getViewWelcomeContent(this.id); + + this.items = []; + + for (const descriptor of descriptors) { + if (descriptor.when === 'default') { + this.defaultItem = { descriptor, visible: true }; + } else { + const visible = descriptor.when ? this.contextKeyService.contextMatchesRules(descriptor.when) : true; + this.items.push({ descriptor, visible }); + } + } + + this._onDidChange.fire(); + } + + private onDidChangeContext(): void { + let didChange = false; + + for (const item of this.items) { + if (!item.descriptor.when || item.descriptor.when === 'default') { + continue; + } + + const visible = this.contextKeyService.contextMatchesRules(item.descriptor.when); + + if (item.visible === visible) { + continue; + } + + item.visible = visible; + didChange = true; + } + + if (didChange) { + this._onDidChange.fire(); + } + } + + dispose(): void { + this.disposables.dispose(); + } +} + +class ViewMenuActions extends CompositeMenuActions { + constructor( + viewId: string, + menuId: MenuId, + contextMenuId: MenuId, + @IContextKeyService contextKeyService: IContextKeyService, + @IMenuService menuService: IMenuService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + ) { + const scopedContextKeyService = contextKeyService.createScoped(); + scopedContextKeyService.createKey('view', viewId); + const viewLocationKey = scopedContextKeyService.createKey('viewLocation', ViewContainerLocationToString(viewDescriptorService.getViewLocationById(viewId)!)); + super(menuId, contextMenuId, { shouldForwardArgs: true }, scopedContextKeyService, menuService); + this._register(scopedContextKeyService); + this._register(Event.filter(viewDescriptorService.onDidChangeLocation, e => e.views.some(view => view.id === viewId))(() => viewLocationKey.set(ViewContainerLocationToString(viewDescriptorService.getViewLocationById(viewId)!)))); + } + +} + +export abstract class ViewPane extends Pane implements IView { + + private static readonly AlwaysShowActionsConfig = 'workbench.view.alwaysShowHeaderActions'; + + private _onDidFocus = this._register(new Emitter()); + readonly onDidFocus: Event = this._onDidFocus.event; + + private _onDidBlur = this._register(new Emitter()); + readonly onDidBlur: Event = this._onDidBlur.event; + + private _onDidChangeBodyVisibility = this._register(new Emitter()); + readonly onDidChangeBodyVisibility: Event = this._onDidChangeBodyVisibility.event; + + protected _onDidChangeTitleArea = this._register(new Emitter()); + readonly onDidChangeTitleArea: Event = this._onDidChangeTitleArea.event; + + protected _onDidChangeViewWelcomeState = this._register(new Emitter()); + readonly onDidChangeViewWelcomeState: Event = this._onDidChangeViewWelcomeState.event; + + private focusedViewContextKey: IContextKey; + + private _isVisible: boolean = false; + readonly id: string; + + private _title: string; + public get title(): string { + return this._title; + } + + private _titleDescription: string | undefined; + public get titleDescription(): string | undefined { + return this._titleDescription; + } + + private readonly menuActions: ViewMenuActions; + private progressBar!: ProgressBar; + private progressIndicator!: IProgressIndicator; + + private toolbar?: ToolBar; + private readonly showActionsAlways: boolean = false; + private headerContainer?: HTMLElement; + private titleContainer?: HTMLElement; + private titleDescriptionContainer?: HTMLElement; + private iconContainer?: HTMLElement; + protected twistiesContainer?: HTMLElement; + + private bodyContainer!: HTMLElement; + private viewWelcomeContainer!: HTMLElement; + private viewWelcomeDisposable: IDisposable = Disposable.None; + private viewWelcomeController: ViewWelcomeController; + + constructor( + options: IViewPaneOptions, + @IKeybindingService protected keybindingService: IKeybindingService, + @IContextMenuService protected contextMenuService: IContextMenuService, + @IConfigurationService protected readonly configurationService: IConfigurationService, + @IContextKeyService protected contextKeyService: IContextKeyService, + @IViewDescriptorService protected viewDescriptorService: IViewDescriptorService, + @IInstantiationService protected instantiationService: IInstantiationService, + @IOpenerService protected openerService: IOpenerService, + @IThemeService protected themeService: IThemeService, + @ITelemetryService protected telemetryService: ITelemetryService, + ) { + super({ ...options, ...{ orientation: viewDescriptorService.getViewLocationById(options.id) === ViewContainerLocation.Panel ? Orientation.HORIZONTAL : Orientation.VERTICAL } }); + + this.id = options.id; + this._title = options.title; + this._titleDescription = options.titleDescription; + this.showActionsAlways = !!options.showActionsAlways; + this.focusedViewContextKey = FocusedViewContext.bindTo(contextKeyService); + + this.menuActions = this._register(instantiationService.createInstance(ViewMenuActions, this.id, options.titleMenuId || MenuId.ViewTitle, MenuId.ViewTitleContext)); + this._register(this.menuActions.onDidChange(() => this.updateActions())); + + this.viewWelcomeController = new ViewWelcomeController(this.id, contextKeyService); + } + + get headerVisible(): boolean { + return super.headerVisible; + } + + set headerVisible(visible: boolean) { + super.headerVisible = visible; + this.element.classList.toggle('merged-header', !visible); + } + + setVisible(visible: boolean): void { + if (this._isVisible !== visible) { + this._isVisible = visible; + + if (this.isExpanded()) { + this._onDidChangeBodyVisibility.fire(visible); + } + } + } + + isVisible(): boolean { + return this._isVisible; + } + + isBodyVisible(): boolean { + return this._isVisible && this.isExpanded(); + } + + setExpanded(expanded: boolean): boolean { + const changed = super.setExpanded(expanded); + if (changed) { + this._onDidChangeBodyVisibility.fire(expanded); + } + if (this.twistiesContainer) { + this.twistiesContainer.classList.remove(...ThemeIcon.asClassNameArray(this.getTwistyIcon(!expanded))); + this.twistiesContainer.classList.add(...ThemeIcon.asClassNameArray(this.getTwistyIcon(expanded))); + } + return changed; + } + + render(): void { + super.render(); + + const focusTracker = trackFocus(this.element); + this._register(focusTracker); + this._register(focusTracker.onDidFocus(() => { + this.focusedViewContextKey.set(this.id); + this._onDidFocus.fire(); + })); + this._register(focusTracker.onDidBlur(() => { + if (this.focusedViewContextKey.get() === this.id) { + this.focusedViewContextKey.reset(); + } + + this._onDidBlur.fire(); + })); + } + + protected renderHeader(container: HTMLElement): void { + this.headerContainer = container; + + this.twistiesContainer = append(container, $(ThemeIcon.asCSSSelector(this.getTwistyIcon(this.isExpanded())))); + + this.renderHeaderTitle(container, this.title); + + const actions = append(container, $('.actions')); + actions.classList.toggle('show', this.showActionsAlways); + this.toolbar = new ToolBar(actions, this.contextMenuService, { + orientation: ActionsOrientation.HORIZONTAL, + actionViewItemProvider: action => this.getActionViewItem(action), + ariaLabel: nls.localize('viewToolbarAriaLabel', "{0} actions", this.title), + getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), + renderDropdownAsChildElement: true + }); + + this._register(this.toolbar); + this.setActions(); + + this._register(addDisposableListener(actions, EventType.CLICK, e => e.preventDefault())); + + this._register(this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerByViewId(this.id)!)!.onDidChangeContainerInfo(({ title }) => { + this.updateTitle(this.title); + })); + + const onDidRelevantConfigurationChange = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ViewPane.AlwaysShowActionsConfig)); + this._register(onDidRelevantConfigurationChange(this.updateActionsVisibility, this)); + this.updateActionsVisibility(); + } + + protected getTwistyIcon(expanded: boolean): ThemeIcon { + return expanded ? viewPaneContainerExpandedIcon : viewPaneContainerCollapsedIcon; + } + + style(styles: IPaneStyles): void { + super.style(styles); + + const icon = this.getIcon(); + if (this.iconContainer) { + const fgColor = styles.headerForeground || this.themeService.getColorTheme().getColor(foreground); + if (URI.isUri(icon)) { + // Apply background color to activity bar item provided with iconUrls + this.iconContainer.style.backgroundColor = fgColor ? fgColor.toString() : ''; + this.iconContainer.style.color = ''; + } else { + // Apply foreground color to activity bar items provided with codicons + this.iconContainer.style.color = fgColor ? fgColor.toString() : ''; + this.iconContainer.style.backgroundColor = ''; + } + } + } + + private getIcon(): ThemeIcon | URI { + return this.viewDescriptorService.getViewDescriptorById(this.id)?.containerIcon || defaultViewIcon; + } + + protected renderHeaderTitle(container: HTMLElement, title: string): void { + this.iconContainer = append(container, $('.icon', undefined)); + const icon = this.getIcon(); + + let cssClass: string | undefined = undefined; + if (URI.isUri(icon)) { + cssClass = `view-${this.id.replace(/[\.\:]/g, '-')}`; + const iconClass = `.pane-header .icon.${cssClass}`; + + createCSSRule(iconClass, ` + mask: ${asCSSUrl(icon)} no-repeat 50% 50%; + mask-size: 24px; + -webkit-mask: ${asCSSUrl(icon)} no-repeat 50% 50%; + -webkit-mask-size: 16px; + `); + } else if (ThemeIcon.isThemeIcon(icon)) { + cssClass = ThemeIcon.asClassName(icon); + } + + if (cssClass) { + this.iconContainer.classList.add(...cssClass.split(' ')); + } + + const calculatedTitle = this.calculateTitle(title); + this.titleContainer = append(container, $('h3.title', { title: calculatedTitle }, calculatedTitle)); + + if (this._titleDescription) { + this.setTitleDescription(this._titleDescription); + } + + this.iconContainer.title = calculatedTitle; + this.iconContainer.setAttribute('aria-label', calculatedTitle); + } + + protected updateTitle(title: string): void { + const calculatedTitle = this.calculateTitle(title); + if (this.titleContainer) { + this.titleContainer.textContent = calculatedTitle; + this.titleContainer.setAttribute('title', calculatedTitle); + } + + if (this.iconContainer) { + this.iconContainer.title = calculatedTitle; + this.iconContainer.setAttribute('aria-label', calculatedTitle); + } + + this._title = title; + this._onDidChangeTitleArea.fire(); + } + + private setTitleDescription(description: string | undefined) { + if (this.titleDescriptionContainer) { + this.titleDescriptionContainer.textContent = description ?? ''; + this.titleDescriptionContainer.setAttribute('title', description ?? ''); + } + else if (description && this.titleContainer) { + this.titleDescriptionContainer = after(this.titleContainer, $('span.description', { title: description }, description)); + } + } + + protected updateTitleDescription(description?: string | undefined): void { + this.setTitleDescription(description); + + this._titleDescription = description; + this._onDidChangeTitleArea.fire(); + } + + private calculateTitle(title: string): string { + const viewContainer = this.viewDescriptorService.getViewContainerByViewId(this.id)!; + const model = this.viewDescriptorService.getViewContainerModel(viewContainer); + const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(this.id); + const isDefault = this.viewDescriptorService.getDefaultContainerById(this.id) === viewContainer; + + if (!isDefault && viewDescriptor?.containerTitle && model.title !== viewDescriptor.containerTitle) { + return `${viewDescriptor.containerTitle}: ${title}`; + } + + return title; + } + + private scrollableElement!: DomScrollableElement; + + protected renderBody(container: HTMLElement): void { + this.bodyContainer = container; + + const viewWelcomeContainer = append(container, $('.welcome-view')); + this.viewWelcomeContainer = $('.welcome-view-content', { tabIndex: 0 }); + this.scrollableElement = this._register(new DomScrollableElement(this.viewWelcomeContainer, { + alwaysConsumeMouseWheel: true, + horizontal: ScrollbarVisibility.Hidden, + vertical: ScrollbarVisibility.Visible, + })); + + append(viewWelcomeContainer, this.scrollableElement.getDomNode()); + + const onViewWelcomeChange = Event.any(this.viewWelcomeController.onDidChange, this.onDidChangeViewWelcomeState); + this._register(onViewWelcomeChange(this.updateViewWelcome, this)); + this.updateViewWelcome(); + } + + protected layoutBody(height: number, width: number): void { + this.viewWelcomeContainer.style.height = `${height}px`; + this.viewWelcomeContainer.style.width = `${width}px`; + this.scrollableElement.scanDomNode(); + } + + getProgressIndicator() { + if (this.progressBar === undefined) { + // Progress bar + this.progressBar = this._register(new ProgressBar(this.element)); + this._register(attachProgressBarStyler(this.progressBar, this.themeService)); + this.progressBar.hide(); + } + + if (this.progressIndicator === undefined) { + this.progressIndicator = this.instantiationService.createInstance(CompositeProgressIndicator, assertIsDefined(this.progressBar), this.id, this.isBodyVisible()); + } + return this.progressIndicator; + } + + protected getProgressLocation(): string { + return this.viewDescriptorService.getViewContainerByViewId(this.id)!.id; + } + + protected getBackgroundColor(): string { + return this.viewDescriptorService.getViewLocationById(this.id) === ViewContainerLocation.Panel ? PANEL_BACKGROUND : SIDE_BAR_BACKGROUND; + } + + focus(): void { + if (this.shouldShowWelcome()) { + this.viewWelcomeContainer.focus(); + } else if (this.element) { + this.element.focus(); + this._onDidFocus.fire(); + } + } + + private setActions(): void { + if (this.toolbar) { + this.toolbar.setActions(prepareActions(this.getActions()), prepareActions(this.getSecondaryActions())); + this.toolbar.context = this.getActionsContext(); + } + } + + private updateActionsVisibility(): void { + if (!this.headerContainer) { + return; + } + const shouldAlwaysShowActions = this.configurationService.getValue('workbench.view.alwaysShowHeaderActions'); + this.headerContainer.classList.toggle('actions-always-visible', shouldAlwaysShowActions); + } + + protected updateActions(): void { + this.setActions(); + this._onDidChangeTitleArea.fire(); + } + + getActions(): IAction[] { + return this.menuActions.getPrimaryActions(); + } + + getSecondaryActions(): IAction[] { + return this.menuActions.getSecondaryActions(); + } + + getContextMenuActions(): IAction[] { + return this.menuActions.getContextMenuActions(); + } + + getActionViewItem(action: IAction): IActionViewItem | undefined { + return createActionViewItem(this.instantiationService, action); + } + + getActionsContext(): unknown { + return undefined; + } + + getOptimalWidth(): number { + return 0; + } + + saveState(): void { + // Subclasses to implement for saving state + } + + private updateViewWelcome(): void { + this.viewWelcomeDisposable.dispose(); + + if (!this.shouldShowWelcome()) { + this.bodyContainer.classList.remove('welcome'); + this.viewWelcomeContainer.innerText = ''; + this.scrollableElement.scanDomNode(); + return; + } + + const contents = this.viewWelcomeController.contents; + + if (contents.length === 0) { + this.bodyContainer.classList.remove('welcome'); + this.viewWelcomeContainer.innerText = ''; + this.scrollableElement.scanDomNode(); + return; + } + + const disposables = new DisposableStore(); + this.bodyContainer.classList.add('welcome'); + this.viewWelcomeContainer.innerText = ''; + + for (const { content, precondition } of contents) { + const lines = content.split('\n'); + + for (let line of lines) { + line = line.trim(); + + if (!line) { + continue; + } + + const linkedText = parseLinkedText(line); + + if (linkedText.nodes.length === 1 && typeof linkedText.nodes[0] !== 'string') { + const node = linkedText.nodes[0]; + const button = new Button(this.viewWelcomeContainer, { title: node.title, supportIcons: true }); + button.label = node.label; + button.onDidClick(_ => { + this.telemetryService.publicLog2<{ viewId: string, uri: string }, WelcomeActionClassification>('views.welcomeAction', { viewId: this.id, uri: node.href }); + this.openerService.open(node.href); + }, null, disposables); + disposables.add(button); + disposables.add(attachButtonStyler(button, this.themeService)); + + if (precondition) { + const updateEnablement = () => button.enabled = this.contextKeyService.contextMatchesRules(precondition); + updateEnablement(); + + const keys = new Set(); + precondition.keys().forEach(key => keys.add(key)); + const onDidChangeContext = Event.filter(this.contextKeyService.onDidChangeContext, e => e.affectsSome(keys)); + onDidChangeContext(updateEnablement, null, disposables); + } + } else { + const p = append(this.viewWelcomeContainer, $('p')); + + for (const node of linkedText.nodes) { + if (typeof node === 'string') { + append(p, document.createTextNode(node)); + } else { + const link = this.instantiationService.createInstance(Link, node); + append(p, link.el); + disposables.add(link); + disposables.add(attachLinkStyler(link, this.themeService)); + + if (precondition && node.href.startsWith('command:')) { + const updateEnablement = () => link.style({ disabled: !this.contextKeyService.contextMatchesRules(precondition) }); + updateEnablement(); + + const keys = new Set(); + precondition.keys().forEach(key => keys.add(key)); + const onDidChangeContext = Event.filter(this.contextKeyService.onDidChangeContext, e => e.affectsSome(keys)); + onDidChangeContext(updateEnablement, null, disposables); + } + } + } + } + } + } + + this.scrollableElement.scanDomNode(); + this.viewWelcomeDisposable = disposables; + } + + shouldShowWelcome(): boolean { + return false; + } +} + +export abstract class ViewAction extends Action2 { + constructor(readonly desc: Readonly & { viewId: string }) { + super(desc); + } + + run(accessor: ServicesAccessor, ...args: any[]) { + const view = accessor.get(IViewsService).getActiveViewWithId(this.desc.viewId); + if (view) { + return this.runInView(accessor, view, ...args); + } + } + + abstract runInView(accessor: ServicesAccessor, view: T, ...args: any[]): any; +} diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 410c6724..e71e07d0 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -6,52 +6,45 @@ import 'vs/css!./media/paneviewlet'; import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; -import { ColorIdentifier, activeContrastBorder, foreground } from 'vs/platform/theme/common/colorRegistry'; -import { attachStyler, IColorMapping, attachButtonStyler, attachLinkStyler, attachProgressBarStyler } 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, PANEL_BACKGROUND, SIDE_BAR_BACKGROUND, PANEL_SECTION_HEADER_FOREGROUND, PANEL_SECTION_HEADER_BACKGROUND, PANEL_SECTION_HEADER_BORDER, PANEL_SECTION_DRAG_AND_DROP_BACKGROUND, PANEL_SECTION_BORDER } from 'vs/workbench/common/theme'; -import { after, append, $, trackFocus, EventType, isAncestor, Dimension, addDisposableListener, createCSSRule, asCSSUrl } from 'vs/base/browser/dom'; -import { IDisposable, combinedDisposable, dispose, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IAction, Separator, IActionViewItem } from 'vs/base/common/actions'; -import { ActionsOrientation, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ColorIdentifier, activeContrastBorder } 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, PANEL_SECTION_HEADER_FOREGROUND, PANEL_SECTION_HEADER_BACKGROUND, PANEL_SECTION_HEADER_BORDER, PANEL_SECTION_DRAG_AND_DROP_BACKGROUND, PANEL_SECTION_BORDER } from 'vs/workbench/common/theme'; +import { EventType, Dimension, addDisposableListener, isAncestor } from 'vs/base/browser/dom'; +import { IDisposable, combinedDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { IAction, IActionViewItem, Separator } from 'vs/base/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IThemeService, Themable, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { PaneView, IPaneViewOptions, IPaneOptions, Pane, IPaneStyles } from 'vs/base/browser/ui/splitview/paneview'; +import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; +import { PaneView, IPaneViewOptions } from 'vs/base/browser/ui/splitview/paneview'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { Extensions as ViewContainerExtensions, IView, FocusedViewContext, IViewDescriptor, ViewContainer, IViewDescriptorService, ViewContainerLocation, IViewPaneContainer, IViewsRegistry, IViewContentDescriptor, IAddedViewDescriptorRef, IViewDescriptorRef, IViewContainerModel, defaultViewIcon } from 'vs/workbench/common/views'; +import { IView, FocusedViewContext, IViewDescriptor, ViewContainer, IViewDescriptorService, ViewContainerLocation, IViewPaneContainer, IAddedViewDescriptorRef, IViewDescriptorRef, IViewContainerModel, IViewsService, ViewContainerLocationToString } from 'vs/workbench/common/views'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyEqualsExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { assertIsDefined } from 'vs/base/common/types'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { Component } from 'vs/workbench/common/component'; -import { MenuId, MenuItemAction, registerAction2, Action2, IAction2Options, SubmenuItemAction } from 'vs/platform/actions/common/actions'; -import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { ViewMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; -import { parseLinkedText } from 'vs/base/common/linkedText'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { Button } from 'vs/base/browser/ui/button/button'; -import { Link } from 'vs/platform/opener/browser/link'; +import { registerAction2, Action2, IAction2Options, IMenuService, MenuId, MenuRegistry, ISubmenuItem, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { CompositeDragAndDropObserver, DragAndDropObserver, toggleDropEffect } from 'vs/workbench/browser/dnd'; import { Orientation } from 'vs/base/browser/ui/sash/sash'; -import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; -import { CompositeProgressIndicator } from 'vs/workbench/services/progress/browser/progressIndicator'; -import { IProgressIndicator } from 'vs/platform/progress/common/progress'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import { ScrollbarVisibility } from 'vs/base/common/scrollable'; -import { URI } from 'vs/base/common/uri'; import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; -import { Codicon } from 'vs/base/common/codicons'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; +import { CompositeMenuActions } from 'vs/workbench/browser/menuActions'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; + +export const ViewsSubMenu = new MenuId('Views'); +MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { + submenu: ViewsSubMenu, + title: nls.localize('views', "Views"), + order: 1, + when: ContextKeyEqualsExpr.create('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar)), +}); export interface IPaneColors extends IColorMapping { dropBackground?: ColorIdentifier; @@ -61,566 +54,6 @@ export interface IPaneColors extends IColorMapping { leftBorder?: ColorIdentifier; } -export interface IViewPaneOptions extends IPaneOptions { - id: string; - showActionsAlways?: boolean; - titleMenuId?: MenuId; -} - -type WelcomeActionClassification = { - viewId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - uri: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; -}; - -const viewPaneContainerExpandedIcon = registerIcon('view-pane-container-expanded', Codicon.chevronDown, nls.localize('viewPaneContainerExpandedIcon', 'Icon for an expanded view pane container.')); -const viewPaneContainerCollapsedIcon = registerIcon('view-pane-container-collapsed', Codicon.chevronRight, nls.localize('viewPaneContainerCollapsedIcon', 'Icon for a collapsed view pane container.')); - -const viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); - -interface IItem { - readonly descriptor: IViewContentDescriptor; - visible: boolean; -} - -class ViewWelcomeController { - - private _onDidChange = new Emitter(); - readonly onDidChange = this._onDidChange.event; - - private defaultItem: IItem | undefined; - private items: IItem[] = []; - get contents(): IViewContentDescriptor[] { - const visibleItems = this.items.filter(v => v.visible); - - if (visibleItems.length === 0 && this.defaultItem) { - return [this.defaultItem.descriptor]; - } - - return visibleItems.map(v => v.descriptor); - } - - private contextKeyService: IContextKeyService; - private disposables = new DisposableStore(); - - constructor( - private id: string, - @IContextKeyService contextKeyService: IContextKeyService, - ) { - this.contextKeyService = contextKeyService.createScoped(); - this.disposables.add(this.contextKeyService); - - contextKeyService.onDidChangeContext(this.onDidChangeContext, this, this.disposables); - Event.filter(viewsRegistry.onDidChangeViewWelcomeContent, id => id === this.id)(this.onDidChangeViewWelcomeContent, this, this.disposables); - this.onDidChangeViewWelcomeContent(); - } - - private onDidChangeViewWelcomeContent(): void { - const descriptors = viewsRegistry.getViewWelcomeContent(this.id); - - this.items = []; - - for (const descriptor of descriptors) { - if (descriptor.when === 'default') { - this.defaultItem = { descriptor, visible: true }; - } else { - const visible = descriptor.when ? this.contextKeyService.contextMatchesRules(descriptor.when) : true; - this.items.push({ descriptor, visible }); - } - } - - this._onDidChange.fire(); - } - - private onDidChangeContext(): void { - let didChange = false; - - for (const item of this.items) { - if (!item.descriptor.when || item.descriptor.when === 'default') { - continue; - } - - const visible = this.contextKeyService.contextMatchesRules(item.descriptor.when); - - if (item.visible === visible) { - continue; - } - - item.visible = visible; - didChange = true; - } - - if (didChange) { - this._onDidChange.fire(); - } - } - - dispose(): void { - this.disposables.dispose(); - } -} - -export abstract class ViewPane extends Pane implements IView { - - private static readonly AlwaysShowActionsConfig = 'workbench.view.alwaysShowHeaderActions'; - - private _onDidFocus = this._register(new Emitter()); - readonly onDidFocus: Event = this._onDidFocus.event; - - private _onDidBlur = this._register(new Emitter()); - readonly onDidBlur: Event = this._onDidBlur.event; - - private _onDidChangeBodyVisibility = this._register(new Emitter()); - readonly onDidChangeBodyVisibility: Event = this._onDidChangeBodyVisibility.event; - - protected _onDidChangeTitleArea = this._register(new Emitter()); - readonly onDidChangeTitleArea: Event = this._onDidChangeTitleArea.event; - - protected _onDidChangeViewWelcomeState = this._register(new Emitter()); - readonly onDidChangeViewWelcomeState: Event = this._onDidChangeViewWelcomeState.event; - - private focusedViewContextKey: IContextKey; - - private _isVisible: boolean = false; - readonly id: string; - - private _title: string; - public get title(): string { - return this._title; - } - - private _titleDescription: string | undefined; - public get titleDescription(): string | undefined { - return this._titleDescription; - } - - private readonly menuActions: ViewMenuActions; - private progressBar!: ProgressBar; - private progressIndicator!: IProgressIndicator; - - private toolbar?: ToolBar; - private readonly showActionsAlways: boolean = false; - private headerContainer?: HTMLElement; - private titleContainer?: HTMLElement; - private titleDescriptionContainer?: HTMLElement; - private iconContainer?: HTMLElement; - protected twistiesContainer?: HTMLElement; - - private bodyContainer!: HTMLElement; - private viewWelcomeContainer!: HTMLElement; - private viewWelcomeDisposable: IDisposable = Disposable.None; - private viewWelcomeController: ViewWelcomeController; - - constructor( - options: IViewPaneOptions, - @IKeybindingService protected keybindingService: IKeybindingService, - @IContextMenuService protected contextMenuService: IContextMenuService, - @IConfigurationService protected readonly configurationService: IConfigurationService, - @IContextKeyService protected contextKeyService: IContextKeyService, - @IViewDescriptorService protected viewDescriptorService: IViewDescriptorService, - @IInstantiationService protected instantiationService: IInstantiationService, - @IOpenerService protected openerService: IOpenerService, - @IThemeService protected themeService: IThemeService, - @ITelemetryService protected telemetryService: ITelemetryService, - ) { - super({ ...options, ...{ orientation: viewDescriptorService.getViewLocationById(options.id) === ViewContainerLocation.Panel ? Orientation.HORIZONTAL : Orientation.VERTICAL } }); - - this.id = options.id; - this._title = options.title; - this._titleDescription = options.titleDescription; - this.showActionsAlways = !!options.showActionsAlways; - this.focusedViewContextKey = FocusedViewContext.bindTo(contextKeyService); - - this.menuActions = this._register(instantiationService.createInstance(ViewMenuActions, this.id, options.titleMenuId || MenuId.ViewTitle, MenuId.ViewTitleContext)); - this._register(this.menuActions.onDidChangeTitle(() => this.updateActions())); - - this.viewWelcomeController = new ViewWelcomeController(this.id, contextKeyService); - } - - get headerVisible(): boolean { - return super.headerVisible; - } - - set headerVisible(visible: boolean) { - super.headerVisible = visible; - this.element.classList.toggle('merged-header', !visible); - } - - setVisible(visible: boolean): void { - if (this._isVisible !== visible) { - this._isVisible = visible; - - if (this.isExpanded()) { - this._onDidChangeBodyVisibility.fire(visible); - } - } - } - - isVisible(): boolean { - return this._isVisible; - } - - isBodyVisible(): boolean { - return this._isVisible && this.isExpanded(); - } - - setExpanded(expanded: boolean): boolean { - const changed = super.setExpanded(expanded); - if (changed) { - this._onDidChangeBodyVisibility.fire(expanded); - } - if (this.twistiesContainer) { - this.twistiesContainer.classList.remove(...ThemeIcon.asClassNameArray(this.getTwistyIcon(!expanded))); - this.twistiesContainer.classList.add(...ThemeIcon.asClassNameArray(this.getTwistyIcon(expanded))); - } - return changed; - } - - render(): void { - super.render(); - - const focusTracker = trackFocus(this.element); - this._register(focusTracker); - this._register(focusTracker.onDidFocus(() => { - this.focusedViewContextKey.set(this.id); - this._onDidFocus.fire(); - })); - this._register(focusTracker.onDidBlur(() => { - if (this.focusedViewContextKey.get() === this.id) { - this.focusedViewContextKey.reset(); - } - - this._onDidBlur.fire(); - })); - } - - protected renderHeader(container: HTMLElement): void { - this.headerContainer = container; - - this.twistiesContainer = append(container, $(ThemeIcon.asCSSSelector(this.getTwistyIcon(this.isExpanded())))); - - this.renderHeaderTitle(container, this.title); - - const actions = append(container, $('.actions')); - actions.classList.toggle('show', this.showActionsAlways); - this.toolbar = new ToolBar(actions, this.contextMenuService, { - orientation: ActionsOrientation.HORIZONTAL, - actionViewItemProvider: action => this.getActionViewItem(action), - ariaLabel: nls.localize('viewToolbarAriaLabel', "{0} actions", this.title), - getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), - renderDropdownAsChildElement: true - }); - - this._register(this.toolbar); - this.setActions(); - - this._register(addDisposableListener(actions, EventType.CLICK, e => e.preventDefault())); - - this._register(this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerByViewId(this.id)!)!.onDidChangeContainerInfo(({ title }) => { - this.updateTitle(this.title); - })); - - const onDidRelevantConfigurationChange = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ViewPane.AlwaysShowActionsConfig)); - this._register(onDidRelevantConfigurationChange(this.updateActionsVisibility, this)); - this.updateActionsVisibility(); - } - - protected getTwistyIcon(expanded: boolean): ThemeIcon { - return expanded ? viewPaneContainerExpandedIcon : viewPaneContainerCollapsedIcon; - } - - style(styles: IPaneStyles): void { - super.style(styles); - - const icon = this.getIcon(); - if (this.iconContainer) { - const fgColor = styles.headerForeground || this.themeService.getColorTheme().getColor(foreground); - if (URI.isUri(icon)) { - // Apply background color to activity bar item provided with iconUrls - this.iconContainer.style.backgroundColor = fgColor ? fgColor.toString() : ''; - this.iconContainer.style.color = ''; - } else { - // Apply foreground color to activity bar items provided with codicons - this.iconContainer.style.color = fgColor ? fgColor.toString() : ''; - this.iconContainer.style.backgroundColor = ''; - } - } - } - - private getIcon(): ThemeIcon | URI { - return this.viewDescriptorService.getViewDescriptorById(this.id)?.containerIcon || defaultViewIcon; - } - - protected renderHeaderTitle(container: HTMLElement, title: string): void { - this.iconContainer = append(container, $('.icon', undefined)); - const icon = this.getIcon(); - - let cssClass: string | undefined = undefined; - if (URI.isUri(icon)) { - cssClass = `view-${this.id.replace(/[\.\:]/g, '-')}`; - const iconClass = `.pane-header .icon.${cssClass}`; - - createCSSRule(iconClass, ` - mask: ${asCSSUrl(icon)} no-repeat 50% 50%; - mask-size: 24px; - -webkit-mask: ${asCSSUrl(icon)} no-repeat 50% 50%; - -webkit-mask-size: 16px; - `); - } else if (ThemeIcon.isThemeIcon(icon)) { - cssClass = ThemeIcon.asClassName(icon); - } - - if (cssClass) { - this.iconContainer.classList.add(...cssClass.split(' ')); - } - - const calculatedTitle = this.calculateTitle(title); - this.titleContainer = append(container, $('h3.title', { title: calculatedTitle }, calculatedTitle)); - - if (this._titleDescription) { - this.setTitleDescription(this._titleDescription); - } - - this.iconContainer.title = calculatedTitle; - this.iconContainer.setAttribute('aria-label', calculatedTitle); - } - - protected updateTitle(title: string): void { - const calculatedTitle = this.calculateTitle(title); - if (this.titleContainer) { - this.titleContainer.textContent = calculatedTitle; - this.titleContainer.setAttribute('title', calculatedTitle); - } - - if (this.iconContainer) { - this.iconContainer.title = calculatedTitle; - this.iconContainer.setAttribute('aria-label', calculatedTitle); - } - - this._title = title; - this._onDidChangeTitleArea.fire(); - } - - private setTitleDescription(description: string | undefined) { - if (this.titleDescriptionContainer) { - this.titleDescriptionContainer.textContent = description ?? ''; - this.titleDescriptionContainer.setAttribute('title', description ?? ''); - } - else if (description && this.titleContainer) { - this.titleDescriptionContainer = after(this.titleContainer, $('span.description', { title: description }, description)); - } - } - - protected updateTitleDescription(description?: string | undefined): void { - this.setTitleDescription(description); - - this._titleDescription = description; - this._onDidChangeTitleArea.fire(); - } - - private calculateTitle(title: string): string { - const viewContainer = this.viewDescriptorService.getViewContainerByViewId(this.id)!; - const model = this.viewDescriptorService.getViewContainerModel(viewContainer); - const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(this.id); - const isDefault = this.viewDescriptorService.getDefaultContainerById(this.id) === viewContainer; - - if (!isDefault && viewDescriptor?.containerTitle && model.title !== viewDescriptor.containerTitle) { - return `${viewDescriptor.containerTitle}: ${title}`; - } - - return title; - } - - private scrollableElement!: DomScrollableElement; - - protected renderBody(container: HTMLElement): void { - this.bodyContainer = container; - - const viewWelcomeContainer = append(container, $('.welcome-view')); - this.viewWelcomeContainer = $('.welcome-view-content', { tabIndex: 0 }); - this.scrollableElement = this._register(new DomScrollableElement(this.viewWelcomeContainer, { - alwaysConsumeMouseWheel: true, - horizontal: ScrollbarVisibility.Hidden, - vertical: ScrollbarVisibility.Visible, - })); - - append(viewWelcomeContainer, this.scrollableElement.getDomNode()); - - const onViewWelcomeChange = Event.any(this.viewWelcomeController.onDidChange, this.onDidChangeViewWelcomeState); - this._register(onViewWelcomeChange(this.updateViewWelcome, this)); - this.updateViewWelcome(); - } - - protected layoutBody(height: number, width: number): void { - this.viewWelcomeContainer.style.height = `${height}px`; - this.viewWelcomeContainer.style.width = `${width}px`; - this.scrollableElement.scanDomNode(); - } - - getProgressIndicator() { - if (this.progressBar === undefined) { - // Progress bar - this.progressBar = this._register(new ProgressBar(this.element)); - this._register(attachProgressBarStyler(this.progressBar, this.themeService)); - this.progressBar.hide(); - } - - if (this.progressIndicator === undefined) { - this.progressIndicator = this.instantiationService.createInstance(CompositeProgressIndicator, assertIsDefined(this.progressBar), this.id, this.isBodyVisible()); - } - return this.progressIndicator; - } - - protected getProgressLocation(): string { - return this.viewDescriptorService.getViewContainerByViewId(this.id)!.id; - } - - protected getBackgroundColor(): string { - return this.viewDescriptorService.getViewLocationById(this.id) === ViewContainerLocation.Panel ? PANEL_BACKGROUND : SIDE_BAR_BACKGROUND; - } - - focus(): void { - if (this.shouldShowWelcome()) { - this.viewWelcomeContainer.focus(); - } else if (this.element) { - this.element.focus(); - this._onDidFocus.fire(); - } - } - - private setActions(): void { - if (this.toolbar) { - this.toolbar.setActions(prepareActions(this.getActions()), prepareActions(this.getSecondaryActions())); - this.toolbar.context = this.getActionsContext(); - } - } - - private updateActionsVisibility(): void { - if (!this.headerContainer) { - return; - } - const shouldAlwaysShowActions = this.configurationService.getValue('workbench.view.alwaysShowHeaderActions'); - this.headerContainer.classList.toggle('actions-always-visible', shouldAlwaysShowActions); - } - - protected updateActions(): void { - this.setActions(); - this._onDidChangeTitleArea.fire(); - } - - getActions(): IAction[] { - return this.menuActions.getPrimaryActions(); - } - - getSecondaryActions(): IAction[] { - return this.menuActions.getSecondaryActions(); - } - - getContextMenuActions(): IAction[] { - return this.menuActions.getContextMenuActions(); - } - - getActionViewItem(action: IAction): IActionViewItem | undefined { - if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action); - } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); - } - return undefined; - } - - getActionsContext(): unknown { - return undefined; - } - - getOptimalWidth(): number { - return 0; - } - - saveState(): void { - // Subclasses to implement for saving state - } - - private updateViewWelcome(): void { - this.viewWelcomeDisposable.dispose(); - - if (!this.shouldShowWelcome()) { - this.bodyContainer.classList.remove('welcome'); - this.viewWelcomeContainer.innerText = ''; - this.scrollableElement.scanDomNode(); - return; - } - - const contents = this.viewWelcomeController.contents; - - if (contents.length === 0) { - this.bodyContainer.classList.remove('welcome'); - this.viewWelcomeContainer.innerText = ''; - this.scrollableElement.scanDomNode(); - return; - } - - const disposables = new DisposableStore(); - this.bodyContainer.classList.add('welcome'); - this.viewWelcomeContainer.innerText = ''; - - for (const { content, precondition } of contents) { - const lines = content.split('\n'); - - for (let line of lines) { - line = line.trim(); - - if (!line) { - continue; - } - - const linkedText = parseLinkedText(line); - - if (linkedText.nodes.length === 1 && typeof linkedText.nodes[0] !== 'string') { - const node = linkedText.nodes[0]; - const button = new Button(this.viewWelcomeContainer, { title: node.title, supportCodicons: true }); - button.label = node.label; - button.onDidClick(_ => { - this.telemetryService.publicLog2<{ viewId: string, uri: string }, WelcomeActionClassification>('views.welcomeAction', { viewId: this.id, uri: node.href }); - this.openerService.open(node.href); - }, null, disposables); - disposables.add(button); - disposables.add(attachButtonStyler(button, this.themeService)); - - if (precondition) { - const updateEnablement = () => button.enabled = this.contextKeyService.contextMatchesRules(precondition); - updateEnablement(); - - const keys = new Set(); - precondition.keys().forEach(key => keys.add(key)); - const onDidChangeContext = Event.filter(this.contextKeyService.onDidChangeContext, e => e.affectsSome(keys)); - onDidChangeContext(updateEnablement, null, disposables); - } - } else { - const p = append(this.viewWelcomeContainer, $('p')); - - for (const node of linkedText.nodes) { - if (typeof node === 'string') { - append(p, document.createTextNode(node)); - } else { - const link = this.instantiationService.createInstance(Link, node); - append(p, link.el); - disposables.add(link); - disposables.add(attachLinkStyler(link, this.themeService)); - } - } - } - } - } - - this.scrollableElement.scanDomNode(); - this.viewWelcomeDisposable = disposables; - } - - shouldShowWelcome(): boolean { - return false; - } -} - export interface IViewPaneContainerOptions extends IPaneViewOptions { mergeViewWithContainerWhenSingleView: boolean; } @@ -860,6 +293,22 @@ class ViewPaneDropOverlay extends Themable { } } +class ViewContainerMenuActions extends CompositeMenuActions { + constructor( + viewContainer: ViewContainer, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IContextKeyService contextKeyService: IContextKeyService, + @IMenuService menuService: IMenuService, + ) { + const scopedContextKeyService = contextKeyService.createScoped(); + scopedContextKeyService.createKey('viewContainer', viewContainer.id); + const viewContainerLocationKey = scopedContextKeyService.createKey('viewContainerLocation', ViewContainerLocationToString(viewDescriptorService.getViewContainerLocation(viewContainer)!)); + super(MenuId.ViewContainerTitle, MenuId.ViewContainerTitleContext, { shouldForwardArgs: true }, scopedContextKeyService, menuService); + this._register(scopedContextKeyService); + this._register(Event.filter(viewDescriptorService.onDidChangeContainerLocation, e => e.viewContainer === viewContainer)(() => viewContainerLocationKey.set(ViewContainerLocationToString(viewDescriptorService.getViewContainerLocation(viewContainer)!)))); + } +} + export class ViewPaneContainer extends Component implements IViewPaneContainer { readonly viewContainer: ViewContainer; @@ -910,6 +359,8 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { return this.paneItems.length; } + private readonly menuActions: ViewContainerMenuActions; + constructor( id: string, private options: IViewPaneContainerOptions, @@ -922,7 +373,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { @IThemeService protected themeService: IThemeService, @IStorageService protected storageService: IStorageService, @IWorkspaceContextService protected contextService: IWorkspaceContextService, - @IViewDescriptorService protected viewDescriptorService: IViewDescriptorService + @IViewDescriptorService protected viewDescriptorService: IViewDescriptorService, ) { super(id, themeService, storageService); @@ -938,6 +389,9 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { this.visibleViewsCountFromCache = this.storageService.getNumber(this.visibleViewsStorageId, StorageScope.WORKSPACE, undefined); this._register(toDisposable(() => this.viewDisposables = dispose(this.viewDisposables))); this.viewContainerModel = this.viewDescriptorService.getViewContainerModel(container); + + this.menuActions = this._register(instantiationService.createInstance(ViewContainerMenuActions, container)); + this._register(this.menuActions.onDidChange(() => this.updateTitleArea())); } create(parent: HTMLElement): void { @@ -1110,57 +564,62 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { let anchor: { x: number, y: number; } = { x: event.posx, y: event.posy }; this.contextMenuService.showContextMenu({ getAnchor: () => anchor, - getActions: () => this.getContextMenuActions() + getActions: () => [...this.getContextMenuActions2()] }); } + getContextMenuActions2(): ReadonlyArray { + return this.menuActions.getContextMenuActions(); + } + getContextMenuActions(viewDescriptor?: IViewDescriptor): IAction[] { - const result: IAction[] = []; + return []; + } - let showHide = true; - if (!viewDescriptor && this.isViewMergedWithContainer()) { - viewDescriptor = this.viewDescriptorService.getViewDescriptorById(this.panes[0].id) || undefined; - showHide = false; + getActions2(): IAction[] { + const result = []; + result.push(...this.menuActions.getPrimaryActions()); + if (this.isViewMergedWithContainer()) { + result.push(...this.paneItems[0].pane.getActions()); } - - if (viewDescriptor) { - if (showHide) { - result.push({ - id: `${viewDescriptor.id}.removeView`, - label: nls.localize('hideView', "Hide"), - enabled: viewDescriptor.canToggleVisibility, - run: () => this.toggleViewVisibility(viewDescriptor!.id) - }); - } - const view = this.getView(viewDescriptor.id); - if (view) { - result.push(...view.getContextMenuActions()); - } - } - - const viewToggleActions = this.getViewsVisibilityActions(); - if (result.length && viewToggleActions.length) { - result.push(new Separator()); - } - - result.push(...viewToggleActions); - return result; } getActions(): IAction[] { - if (this.isViewMergedWithContainer()) { - return this.paneItems[0].pane.getActions(); - } - return []; } - getSecondaryActions(): IAction[] { - if (this.isViewMergedWithContainer()) { - return this.paneItems[0].pane.getSecondaryActions(); + getSecondaryActions2(): IAction[] { + const viewPaneActions = this.isViewMergedWithContainer() ? this.paneItems[0].pane.getSecondaryActions() : []; + let menuActions = this.menuActions.getSecondaryActions(); + + const viewsSubmenuActionIndex = menuActions.findIndex(action => action instanceof SubmenuItemAction && action.item.submenu === ViewsSubMenu); + if (viewsSubmenuActionIndex !== -1) { + const viewsSubmenuAction = menuActions[viewsSubmenuActionIndex]; + if (viewsSubmenuAction.actions.some(({ enabled }) => enabled)) { + if (menuActions.length === 1 && viewPaneActions.length === 0) { + menuActions = viewsSubmenuAction.actions.slice(); + } else if (viewsSubmenuActionIndex !== 0) { + menuActions = [viewsSubmenuAction, ...menuActions.slice(0, viewsSubmenuActionIndex), ...menuActions.slice(viewsSubmenuActionIndex + 1)]; + } + } else { + // Remove views submenu if none of the actions are enabled + menuActions.splice(viewsSubmenuActionIndex, 1); + } } + if (menuActions.length && viewPaneActions.length) { + return [ + ...menuActions, + new Separator(), + ...viewPaneActions + ]; + } + + return menuActions.length ? menuActions : viewPaneActions; + } + + getSecondaryActions(): IAction[] { return []; } @@ -1168,22 +627,11 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { return undefined; } - getViewsVisibilityActions(): IAction[] { - return this.viewContainerModel.activeViewDescriptors.map(viewDescriptor => ({ - id: `${viewDescriptor.id}.toggleVisibility`, - label: viewDescriptor.name, - checked: this.viewContainerModel.isVisible(viewDescriptor.id), - enabled: viewDescriptor.canToggleVisibility && (!this.viewContainerModel.isVisible(viewDescriptor.id) || this.viewContainerModel.visibleViewDescriptors.length > 1), - run: () => this.toggleViewVisibility(viewDescriptor.id) - })); - } - getActionViewItem(action: IAction): IActionViewItem | undefined { if (this.isViewMergedWithContainer()) { return this.paneItems[0].pane.getActionViewItem(action); } - - return undefined; + return createActionViewItem(this.instantiationService, action); } focus(): void { @@ -1321,11 +769,11 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { this.storageService.store(this.visibleViewsStorageId, this.length, StorageScope.WORKSPACE, StorageTarget.USER); } - private onContextMenu(event: StandardMouseEvent, viewDescriptor: IViewDescriptor): void { + private onContextMenu(event: StandardMouseEvent, viewPane: ViewPane): void { event.stopPropagation(); event.preventDefault(); - const actions: IAction[] = this.getContextMenuActions(viewDescriptor); + const actions: IAction[] = viewPane.getContextMenuActions(); let anchor: { x: number, y: number } = { x: event.posx, y: event.posy }; this.contextMenuService.showContextMenu({ @@ -1364,7 +812,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { const contextMenuDisposable = addDisposableListener(pane.draggableElement, 'contextmenu', e => { e.stopPropagation(); e.preventDefault(); - this.onContextMenu(new StandardMouseEvent(e), viewDescriptor); + this.onContextMenu(new StandardMouseEvent(e), pane); }); const collapseDisposable = Event.latch(Event.map(pane.onDidChange, () => !pane.isExpanded()))(collapsed => { @@ -1401,7 +849,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } } - protected toggleViewVisibility(viewId: string): void { + toggleViewVisibility(viewId: string): void { // Check if view is active if (this.viewContainerModel.activeViewDescriptors.some(viewDescriptor => viewDescriptor.id === viewId)) { const visible = !this.viewContainerModel.isVisible(viewId); @@ -1641,7 +1089,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } } - private isViewMergedWithContainer(): boolean { + isViewMergedWithContainer(): boolean { if (!(this.options.mergeViewWithContainerWhenSingleView && this.paneItems.length === 1)) { return false; } @@ -1665,6 +1113,21 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } } +export abstract class ViewPaneContainerAction extends Action2 { + constructor(readonly desc: Readonly & { viewPaneContainerId: string }) { + super(desc); + } + + run(accessor: ServicesAccessor, ...args: any[]) { + const viewPaneContainer = accessor.get(IViewsService).getActiveViewPaneContainerWithId(this.desc.viewPaneContainerId); + if (viewPaneContainer) { + return this.runInViewPaneContainer(accessor, viewPaneContainer, ...args); + } + } + + abstract runInViewPaneContainer(accessor: ServicesAccessor, viewPaneContainer: T, ...args: any[]): any; +} + class MoveViewPosition extends Action2 { constructor(desc: Readonly, private readonly offset: number) { super(desc); diff --git a/src/vs/workbench/browser/parts/views/viewsService.ts b/src/vs/workbench/browser/parts/views/viewsService.ts index 687fbdb8..3f47ba10 100644 --- a/src/vs/workbench/browser/parts/views/viewsService.ts +++ b/src/vs/workbench/browser/parts/views/viewsService.ts @@ -196,7 +196,9 @@ export class ViewsService extends Disposable implements IViewsService { ContextKeyExpr.equals('view', viewDescriptor.id), ContextKeyExpr.equals(`${viewDescriptor.id}.defaultViewLocation`, false) ) - ) + ), + group: '1_hide', + order: 2 }], }); } @@ -392,12 +394,29 @@ export class ViewsService extends Disposable implements IViewsService { getViewProgressIndicator(viewId: string): IProgressIndicator | undefined { const viewContainer = this.viewDescriptorService.getViewContainerByViewId(viewId); - if (viewContainer === null) { + if (!viewContainer) { return undefined; } - const view = this.viewPaneContainers.get(viewContainer.id)?.viewPaneContainer?.getView(viewId); - return view?.getProgressIndicator(); + const viewPaneContainer = this.viewPaneContainers.get(viewContainer.id)?.viewPaneContainer; + if (!viewPaneContainer) { + return undefined; + } + + const view = viewPaneContainer.getView(viewId); + if (!view) { + return undefined; + } + + if (viewPaneContainer.isViewMergedWithContainer()) { + return this.getViewContainerProgressIndicator(viewContainer); + } + + return view.getProgressIndicator(); + } + + private getViewContainerProgressIndicator(viewContainer: ViewContainer): IProgressIndicator | undefined { + return this.viewDescriptorService.getViewContainerLocation(viewContainer) === ViewContainerLocation.Sidebar ? this.viewletService.getProgressIndicator(viewContainer.id) : this.panelService.getProgressIndicator(viewContainer.id); } private registerViewletOrPanel(viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation): void { @@ -451,7 +470,6 @@ export class ViewsService extends Disposable implements IViewsService { undefined, viewContainer.order, viewContainer.requestedIndex, - viewContainer.focusCommand?.id, )); } diff --git a/src/vs/workbench/browser/parts/views/viewsViewlet.ts b/src/vs/workbench/browser/parts/views/viewsViewlet.ts index 47b8f051..d6a6f07e 100644 --- a/src/vs/workbench/browser/parts/views/viewsViewlet.ts +++ b/src/vs/workbench/browser/parts/views/viewsViewlet.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IAction } from 'vs/base/common/actions'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IViewDescriptor, IViewDescriptorService, IAddedViewDescriptorRef } from 'vs/workbench/common/views'; @@ -12,7 +11,8 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { ViewPaneContainer, ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { Event } from 'vs/base/common/event'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; @@ -81,18 +81,6 @@ export abstract class FilterViewPaneContainer extends ViewPaneContainer { this.getViewsForTarget(newFilterValue).forEach(item => this.viewContainerModel.setVisible(item.id, true)); } - getContextMenuActions(): IAction[] { - const result: IAction[] = Array.from(this.constantViewDescriptors.values()).map(viewDescriptor => ({ - id: `${viewDescriptor.id}.toggleVisibility`, - label: viewDescriptor.name, - checked: this.viewContainerModel.isVisible(viewDescriptor.id), - enabled: viewDescriptor.canToggleVisibility, - run: () => this.toggleViewVisibility(viewDescriptor.id) - })); - - return result; - } - private getViewsForTarget(target: string[]): IViewDescriptor[] { const views: IViewDescriptor[] = []; for (let i = 0; i < target.length; i++) { @@ -140,7 +128,4 @@ export abstract class FilterViewPaneContainer extends ViewPaneContainer { abstract getTitle(): string; - getViewsVisibilityActions(): IAction[] { - return []; - } } diff --git a/src/vs/workbench/browser/style.ts b/src/vs/workbench/browser/style.ts index 0517f059..4f1ecbcd 100644 --- a/src/vs/workbench/browser/style.ts +++ b/src/vs/workbench/browser/style.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/style'; -import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { iconForeground, foreground, selectionBackground, focusBorder, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, listHighlightForeground, inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry'; import { WORKBENCH_BACKGROUND, TITLE_BAR_ACTIVE_BACKGROUND } from 'vs/workbench/common/theme'; import { isWeb, isIOS, isMacintosh, isWindows } from 'vs/base/common/platform'; @@ -13,7 +13,7 @@ import { createMetaElement } from 'vs/base/browser/dom'; import { isSafari, isStandalone } from 'vs/base/browser/browser'; import { ColorScheme } from 'vs/platform/theme/common/theme'; -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { // Foreground const windowForeground = theme.getColor(foreground); diff --git a/src/vs/workbench/browser/viewlet.ts b/src/vs/workbench/browser/viewlet.ts index 01ef1665..79ec5209 100644 --- a/src/vs/workbench/browser/viewlet.ts +++ b/src/vs/workbench/browser/viewlet.ts @@ -3,23 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; -import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; +import { Action } from 'vs/base/common/actions'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { CompositeDescriptor, CompositeRegistry } from 'vs/workbench/browser/composite'; -import { IConstructorSignature0, IInstantiationService, BrandedService } from 'vs/platform/instantiation/common/instantiation'; -import { ToggleSidebarVisibilityAction, ToggleSidebarPositionAction } from 'vs/workbench/browser/actions/layoutActions'; +import { IConstructorSignature0, IInstantiationService, BrandedService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { URI } from 'vs/base/common/uri'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; -import { AbstractTree } from 'vs/base/browser/ui/tree/abstractTree'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -28,6 +24,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { PaneComposite } from 'vs/workbench/browser/panecomposite'; import { Event } from 'vs/base/common/event'; import { FilterViewPaneContainer } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { Action2 } from 'vs/platform/actions/common/actions'; export abstract class Viewlet extends PaneComposite implements IViewlet { @@ -52,40 +49,6 @@ export abstract class Viewlet extends PaneComposite implements IViewlet { })); } } - - getContextMenuActions(): IAction[] { - const parentActions = [...super.getContextMenuActions()]; - if (parentActions.length) { - parentActions.push(new Separator()); - } - - const toggleSidebarPositionAction = new ToggleSidebarPositionAction(ToggleSidebarPositionAction.ID, ToggleSidebarPositionAction.getLabel(this.layoutService), this.layoutService, this.configurationService); - return [...parentActions, toggleSidebarPositionAction, - { - id: ToggleSidebarVisibilityAction.ID, - label: nls.localize('compositePart.hideSideBarLabel', "Hide Side Bar"), - enabled: true, - run: () => this.layoutService.setSideBarHidden(true) - }]; - } - - getSecondaryActions(): IAction[] { - const viewVisibilityActions = this.viewPaneContainer.getViewsVisibilityActions(); - const secondaryActions = this.viewPaneContainer.getSecondaryActions(); - if (viewVisibilityActions.length <= 1 || viewVisibilityActions.every(({ enabled }) => !enabled)) { - return secondaryActions; - } - - if (secondaryActions.length === 0) { - return viewVisibilityActions; - } - - return [ - new SubmenuAction('workbench.views', nls.localize('views', "Views"), viewVisibilityActions), - new Separator(), - ...secondaryActions - ]; - } } /** @@ -115,7 +78,7 @@ export class ViewletDescriptor extends CompositeDescriptor { requestedIndex?: number, readonly iconUrl?: URI ) { - super(ctor, id, name, cssClass, order, requestedIndex, id); + super(ctor, id, name, cssClass, order, requestedIndex); } } @@ -152,7 +115,6 @@ export class ViewletRegistry extends CompositeRegistry { getViewlets(): ViewletDescriptor[] { return this.getComposites() as ViewletDescriptor[]; } - } Registry.add(Extensions.Viewlets, new ViewletRegistry()); @@ -176,7 +138,7 @@ export class ShowViewletAction extends Action { async run(): Promise { // Pass focus to viewlet if not open or focused - if (this.otherViewletShowing() || !this.sidebarHasFocus()) { + if (otherViewletShowing(this.viewletService, this.viewletId) || !sidebarHasFocus(this.viewletService, this.layoutService)) { await this.viewletService.openViewlet(this.viewletId, true); return; } @@ -184,28 +146,42 @@ export class ShowViewletAction extends Action { // Otherwise pass focus to editor group this.editorGroupService.activeGroup.focus(); } +} - private otherViewletShowing(): boolean { - const activeViewlet = this.viewletService.getActiveViewlet(); +/** + * A reusable action to show a viewlet with a specific id. + */ +export abstract class ShowViewletAction2 extends Action2 { + /** + * Gets the viewlet ID to show. + */ + protected abstract viewletId(): string; - return !activeViewlet || activeViewlet.getId() !== this.viewletId; - } + public async run(accessor: ServicesAccessor): Promise { + const viewletService = accessor.get(IViewletService); + const editorGroupService = accessor.get(IEditorGroupsService); + const layoutService = accessor.get(IWorkbenchLayoutService); - private sidebarHasFocus(): boolean { - const activeViewlet = this.viewletService.getActiveViewlet(); - const activeElement = document.activeElement; - const sidebarPart = this.layoutService.getContainer(Parts.SIDEBAR_PART); + // Pass focus to viewlet if not open or focused + if (otherViewletShowing(viewletService, this.viewletId()) || !sidebarHasFocus(viewletService, layoutService)) { + await viewletService.openViewlet(this.viewletId(), true); + return; + } - return !!(activeViewlet && activeElement && sidebarPart && DOM.isAncestor(activeElement, sidebarPart)); + // Otherwise pass focus to editor group + editorGroupService.activeGroup.focus(); } } -export class CollapseAction extends Action { - // We need a tree getter because the action is sometimes instantiated too early - constructor(treeGetter: () => AsyncDataTree | AbstractTree, enabled: boolean, clazz?: string) { - super('workbench.action.collapse', nls.localize('collapse', "Collapse All"), clazz, enabled, async () => { - const tree = treeGetter(); - tree.collapseAll(); - }); - } -} +const otherViewletShowing = (viewletService: IViewletService, viewletId: string): boolean => { + const activeViewlet = viewletService.getActiveViewlet(); + return !activeViewlet || activeViewlet.getId() !== viewletId; +}; + +const sidebarHasFocus = (viewletService: IViewletService, layoutService: IWorkbenchLayoutService): boolean => { + const activeViewlet = viewletService.getActiveViewlet(); + const activeElement = document.activeElement; + const sidebarPart = layoutService.getContainer(Parts.SIDEBAR_PART); + + return !!(activeViewlet && activeElement && sidebarPart && DOM.isAncestor(activeElement, sidebarPart)); +}; diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index e7d260b0..78bfe33c 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { mark } from 'vs/base/common/performance'; -import { hash } from 'vs/base/common/hash'; -import { domContentLoaded, addDisposableListener, EventType, EventHelper, detectFullscreen, addDisposableThrottledListener } from 'vs/base/browser/dom'; +import { domContentLoaded, detectFullscreen, getCookieValue } from 'vs/base/browser/dom'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILogService, ConsoleLogService, MultiplexLogService, getLogLevel } from 'vs/platform/log/common/log'; import { ConsoleLogInAutomationService } from 'vs/platform/log/browser/log'; @@ -24,10 +23,9 @@ import { IFileService, IFileSystemProvider } from 'vs/platform/files/common/file import { FileService } from 'vs/platform/files/common/fileService'; import { Schemas } from 'vs/base/common/network'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { onUnexpectedError } from 'vs/base/common/errors'; import { setFullscreen } from 'vs/base/browser/browser'; -import { isIOS, isMacintosh } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService'; @@ -37,12 +35,11 @@ import { SignService } from 'vs/platform/sign/browser/signService'; import type { IWorkbenchConstructionOptions, IWorkspace, IWorkbench } from 'vs/workbench/workbench.web.api'; import { BrowserStorageService } from 'vs/platform/storage/browser/storageService'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { registerWindowDriver } from 'vs/platform/driver/browser/driver'; import { BufferLogService } from 'vs/platform/log/common/bufferLog'; import { FileLogService } from 'vs/platform/log/common/fileLogService'; import { toLocalISOString } from 'vs/base/common/date'; import { isWorkspaceToOpen, isFolderToOpen } from 'vs/platform/windows/common/windows'; -import { getWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; +import { getSingleFolderWorkspaceIdentifier, getWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; import { coalesce } from 'vs/base/common/arrays'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -61,6 +58,8 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { UriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentityService'; +import { BrowserWindow } from 'vs/workbench/browser/window'; +import { ITimerService } from 'vs/workbench/services/timer/browser/timerService'; class BrowserMain extends Disposable { @@ -83,35 +82,40 @@ class BrowserMain extends Disposable { const services = await this.initServices(); await domContentLoaded(); - mark('willStartWorkbench'); + mark('code/willStartWorkbench'); // Create Workbench - const workbench = new Workbench( - this.domElement, - services.serviceCollection, - services.logService - ); + const workbench = new Workbench(this.domElement, services.serviceCollection, services.logService); // Listeners this.registerListeners(workbench, services.storageService, services.logService); - // Driver - if (this.configuration.driver) { - (async () => this._register(await registerWindowDriver()))(); - } - // Startup const instantiationService = workbench.startup(); + // Window + this._register(instantiationService.createInstance(BrowserWindow)); + + // Logging + services.logService.trace('workbench configuration', JSON.stringify(this.configuration)); + // Return API Facade return instantiationService.invokeFunction(accessor => { const commandService = accessor.get(ICommandService); const lifecycleService = accessor.get(ILifecycleService); + const timerService = accessor.get(ITimerService); return { commands: { executeCommand: (command, ...args) => commandService.executeCommand(command, ...args) }, + env: { + async retrievePerformanceMarks() { + await timerService.whenReady(); + + return timerService.getPerformanceMarks(); + } + }, shutdown: () => lifecycleService.shutdown() }; }); @@ -119,46 +123,17 @@ class BrowserMain extends Disposable { private registerListeners(workbench: Workbench, storageService: BrowserStorageService, logService: ILogService): void { - // Layout - const viewport = isIOS && window.visualViewport ? window.visualViewport /** Visual viewport */ : window /** Layout viewport */; - this._register(addDisposableListener(viewport, EventType.RESIZE, () => { - logService.trace(`web.main#${isIOS && window.visualViewport ? 'visualViewport' : 'window'}Resize`); - workbench.layout(); - })); - - // Prevent the back/forward gestures in macOS - this._register(addDisposableListener(this.domElement, EventType.WHEEL, e => e.preventDefault(), { passive: false })); - - // Prevent native context menus in web - this._register(addDisposableListener(this.domElement, EventType.CONTEXT_MENU, e => EventHelper.stop(e, true))); - - // Prevent default navigation on drop - this._register(addDisposableListener(this.domElement, EventType.DROP, e => EventHelper.stop(e, true))); - // Workbench Lifecycle this._register(workbench.onBeforeShutdown(event => { if (storageService.hasPendingUpdate) { - logService.warn('Unload veto: pending storage update'); - event.veto(true); // prevent data loss from pending storage update + event.veto(true, 'veto.pendingStorageUpdate'); // prevent data loss from pending storage update } })); - this._register(workbench.onWillShutdown(() => { - storageService.close(); - })); + this._register(workbench.onWillShutdown(() => storageService.close())); this._register(workbench.onShutdown(() => this.dispose())); - - // Fullscreen (Browser) - [EventType.FULLSCREEN_CHANGE, EventType.WK_FULLSCREEN_CHANGE].forEach(event => { - this._register(addDisposableListener(document, event, () => setFullscreen(!!detectFullscreen()))); - }); - - // Fullscreen (Native) - this._register(addDisposableThrottledListener(viewport, EventType.RESIZE, () => { - setFullscreen(!!detectFullscreen()); - }, undefined, isMacintosh ? 2000 /* adjust for macOS animation */ : 800 /* can be throttled */)); } - private async initServices(): Promise<{ serviceCollection: ServiceCollection, configurationService: IConfigurationService, logService: ILogService, storageService: BrowserStorageService }> { + private async initServices(): Promise<{ serviceCollection: ServiceCollection, configurationService: IWorkbenchConfigurationService, logService: ILogService, storageService: BrowserStorageService }> { const serviceCollection = new ServiceCollection(); // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -166,7 +141,7 @@ class BrowserMain extends Disposable { // CONTRIBUTE IT VIA WORKBENCH.WEB.MAIN.TS AND registerSingleton(). // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - const payload = await this.resolveWorkspaceInitializationPayload(); + const payload = this.resolveWorkspaceInitializationPayload(); // Product const productService: IProductService = { _serviceBrand: undefined, ...product, ...this.configuration.productConfiguration }; @@ -181,9 +156,8 @@ class BrowserMain extends Disposable { const logService = new BufferLogService(getLogLevel(environmentService)); serviceCollection.set(ILogService, logService); - const connectionToken = environmentService.options.connectionToken || this.getCookieValue('vscode-tkn'); - // Remote + const connectionToken = environmentService.options.connectionToken || getCookieValue('vscode-tkn'); const remoteAuthorityResolverService = new RemoteAuthorityResolverService(connectionToken, this.configuration.resourceUriProvider); serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService); @@ -212,7 +186,7 @@ class BrowserMain extends Disposable { serviceCollection.set(IWorkspaceContextService, service); // Configuration - serviceCollection.set(IConfigurationService, service); + serviceCollection.set(IWorkbenchConfigurationService, service); return service; }), @@ -239,14 +213,16 @@ class BrowserMain extends Disposable { serviceCollection.set(IUserDataInitializationService, userDataInitializationService); if (await userDataInitializationService.requiresInitialization()) { - mark('willInitRequiredUserData'); + mark('code/willInitRequiredUserData'); + // Initialize required resources - settings & global state await userDataInitializationService.initializeRequiredResources(); // Important: Reload only local user configuration after initializing // Reloading complete configuraiton blocks workbench until remote configuration is loaded. await configurationService.reloadLocalUserConfiguration(); - mark('didInitRequiredUserData'); + + mark('code/didInitRequiredUserData'); } return { serviceCollection, configurationService, logService, storageService }; @@ -261,8 +237,9 @@ class BrowserMain extends Disposable { try { indexedDBLogProvider = await indexedDB.createFileSystemProvider(logsPath.scheme, INDEXEDDB_LOGS_OBJECT_STORE); } catch (error) { - console.error(error); + onUnexpectedError(error); } + if (indexedDBLogProvider) { fileService.registerProvider(logsPath.scheme, indexedDBLogProvider); } else { @@ -279,6 +256,7 @@ class BrowserMain extends Disposable { const connection = remoteAgentService.getConnection(); if (connection) { + // Remote file system const remoteFileSystemProvider = this._register(new RemoteFileSystemProvider(remoteAgentService)); fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider); @@ -289,8 +267,9 @@ class BrowserMain extends Disposable { try { indexedDBUserDataProvider = await indexedDB.createFileSystemProvider(Schemas.userData, INDEXEDDB_USERDATA_OBJECT_STORE); } catch (error) { - console.error(error); + onUnexpectedError(error); } + fileService.registerProvider(Schemas.userData, indexedDBUserDataProvider || new InMemoryFileSystemProvider()); if (indexedDBUserDataProvider) { registerAction2(class ResetUserDataAction extends Action2 { @@ -304,21 +283,22 @@ class BrowserMain extends Disposable { } }); } + async run(accessor: ServicesAccessor): Promise { const dialogService = accessor.get(IDialogService); const hostService = accessor.get(IHostService); const result = await dialogService.confirm({ message: localize('reset user data message', "Would you like to reset your data (settings, keybindings, extensions, snippets and UI State) and reload?") }); + if (result.confirmed) { - await indexedDBUserDataProvider!.reset(); + await indexedDBUserDataProvider?.reset(); } + hostService.reload(); } }); } - - fileService.registerProvider(Schemas.userData, indexedDBUserDataProvider || new InMemoryFileSystemProvider()); } private async createStorageService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, fileService: IFileService, logService: ILogService): Promise { @@ -351,7 +331,7 @@ class BrowserMain extends Disposable { } } - private async resolveWorkspaceInitializationPayload(): Promise { + private resolveWorkspaceInitializationPayload(): IWorkspaceInitializationPayload { let workspace: IWorkspace | undefined = undefined; if (this.configuration.workspaceProvider) { workspace = this.configuration.workspaceProvider.workspace; @@ -364,18 +344,11 @@ class BrowserMain extends Disposable { // Single-folder workspace if (workspace && isFolderToOpen(workspace)) { - const id = hash(workspace.folderUri.toString()).toString(16); - return { id, folder: workspace.folderUri }; + return getSingleFolderWorkspaceIdentifier(workspace.folderUri); } return { id: 'empty-window' }; } - - private getCookieValue(name: string): string | undefined { - const match = document.cookie.match('(^|[^;]+)\\s*' + name + '\\s*=\\s*([^;]+)'); // See https://stackoverflow.com/a/25490531 - - return match ? match.pop() : undefined; - } } export function main(domElement: HTMLElement, options: IWorkbenchConstructionOptions): Promise { diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts new file mode 100644 index 00000000..337771b2 --- /dev/null +++ b/src/vs/workbench/browser/window.ts @@ -0,0 +1,161 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { setFullscreen } from 'vs/base/browser/browser'; +import { addDisposableListener, addDisposableThrottledListener, detectFullscreen, EventHelper, EventType, windowOpenNoOpener } from 'vs/base/browser/dom'; +import { domEvent } from 'vs/base/browser/event'; +import { timeout } from 'vs/base/common/async'; +import { Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { isIOS, isMacintosh } from 'vs/base/common/platform'; +import Severity from 'vs/base/common/severity'; +import { localize } from 'vs/nls'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { registerWindowDriver } from 'vs/platform/driver/browser/driver'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; +import { BrowserLifecycleService } from 'vs/workbench/services/lifecycle/browser/lifecycleService'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; + +export class BrowserWindow extends Disposable { + + constructor( + @IOpenerService private readonly openerService: IOpenerService, + @ILifecycleService private readonly lifecycleService: BrowserLifecycleService, + @IDialogService private readonly dialogService: IDialogService, + @IHostService private readonly hostService: IHostService, + @ILabelService private readonly labelService: ILabelService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @ILogService private readonly logService: ILogService, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService + ) { + super(); + + this.registerListeners(); + this.create(); + } + + private registerListeners(): void { + + // Lifecycle + this._register(this.lifecycleService.onWillShutdown(() => this.onWillShutdown())); + + // Layout + const viewport = isIOS && window.visualViewport ? window.visualViewport /** Visual viewport */ : window /** Layout viewport */; + this._register(addDisposableListener(viewport, EventType.RESIZE, () => this.onWindowResize())); + + // Prevent the back/forward gestures in macOS + this._register(addDisposableListener(this.layoutService.getWorkbenchContainer(), EventType.WHEEL, e => e.preventDefault(), { passive: false })); + + // Prevent native context menus in web + this._register(addDisposableListener(this.layoutService.getWorkbenchContainer(), EventType.CONTEXT_MENU, e => EventHelper.stop(e, true))); + + // Prevent default navigation on drop + this._register(addDisposableListener(this.layoutService.getWorkbenchContainer(), EventType.DROP, e => EventHelper.stop(e, true))); + + // Fullscreen (Browser) + [EventType.FULLSCREEN_CHANGE, EventType.WK_FULLSCREEN_CHANGE].forEach(event => { + this._register(addDisposableListener(document, event, () => setFullscreen(!!detectFullscreen()))); + }); + + // Fullscreen (Native) + this._register(addDisposableThrottledListener(viewport, EventType.RESIZE, () => { + setFullscreen(!!detectFullscreen()); + }, undefined, isMacintosh ? 2000 /* adjust for macOS animation */ : 800 /* can be throttled */)); + } + + private onWindowResize(): void { + this.logService.trace(`web.main#${isIOS && window.visualViewport ? 'visualViewport' : 'window'}Resize`); + + this.layoutService.layout(); + } + + private onWillShutdown(): void { + + // Try to detect some user interaction with the workbench + // when shutdown has happened to not show the dialog e.g. + // when navigation takes a longer time. + Event.toPromise(Event.any( + Event.once(domEvent(document.body, EventType.KEY_DOWN, true)), + Event.once(domEvent(document.body, EventType.MOUSE_DOWN, true)) + )).then(async () => { + + // Delay the dialog in case the user interacted + // with the page before it transitioned away + await timeout(3000); + + // This should normally not happen, but if for some reason + // the workbench was shutdown while the page is still there, + // inform the user that only a reload can bring back a working + // state. + const res = await this.dialogService.show( + Severity.Error, + localize('shutdownError', "An unexpected error occurred that requires a reload of this page."), + [ + localize('reload', "Reload") + ], + { + detail: localize('shutdownErrorDetail', "The workbench was unexpectedly disposed while running.") + } + ); + + if (res.choice === 0) { + this.hostService.reload(); + } + }); + } + + private create(): void { + + // Driver + if (this.environmentService.options?.driver) { + (async () => this._register(await registerWindowDriver()))(); + } + + // Handle open calls + this.setupOpenHandlers(); + + // Label formatting + this.registerLabelFormatters(); + } + + private setupOpenHandlers(): void { + + // We need to ignore the `beforeunload` event while + // we handle external links to open specifically for + // the case of application protocols that e.g. invoke + // vscode itself. We do not want to open these links + // in a new window because that would leave a blank + // window to the user, but using `window.location.href` + // will trigger the `beforeunload`. + this.openerService.setDefaultExternalOpener({ + openExternal: async (href: string) => { + if (matchesScheme(href, Schemas.http) || matchesScheme(href, Schemas.https)) { + windowOpenNoOpener(href); + } else { + this.lifecycleService.withExpectedUnload(() => window.location.href = href); + } + + return true; + } + }); + } + + private registerLabelFormatters() { + this.labelService.registerFormatter({ + scheme: Schemas.userData, + priority: true, + formatting: { + label: '${scheme}:${path}', + separator: '/', + } + }); + } +} diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index fdc99470..03297afe 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -33,14 +33,29 @@ import { isStandalone } from 'vs/base/browser/browser'; 'description': nls.localize('showEditorTabs', "Controls whether opened editors should show in tabs or not."), 'default': true }, + 'workbench.editor.wrapTabs': { + 'type': 'boolean', + 'markdownDescription': nls.localize('wrapTabs', "Controls whether tabs should be wrapped over multiple lines when exceeding available space or whether a scrollbar should appear instead. This value is ignored when `#workbench.editor.showTabs#` is disabled."), + 'default': false + }, 'workbench.editor.scrollToSwitchTabs': { 'type': 'boolean', - 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'scrollToSwitchTabs' }, "Controls whether scrolling over tabs will open them or not. By default tabs will only reveal upon scrolling, but not open. You can press and hold the Shift-key while scrolling to change this behaviour for that duration. This value is ignored when `#workbench.editor.showTabs#` is `false`."), + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'scrollToSwitchTabs' }, "Controls whether scrolling over tabs will open them or not. By default tabs will only reveal upon scrolling, but not open. You can press and hold the Shift-key while scrolling to change this behaviour for that duration. This value is ignored when `#workbench.editor.showTabs#` is disabled."), 'default': false }, 'workbench.editor.highlightModifiedTabs': { 'type': 'boolean', - 'markdownDescription': nls.localize('highlightModifiedTabs', "Controls whether a top border is drawn on modified (dirty) editor tabs or not. This value is ignored when `#workbench.editor.showTabs#` is `false`."), + 'markdownDescription': nls.localize('highlightModifiedTabs', "Controls whether a top border is drawn on modified (dirty) editor tabs or not. This value is ignored when `#workbench.editor.showTabs#` is disabled."), + 'default': false + }, + 'workbench.editor.decorations.badges': { + 'type': 'boolean', + 'markdownDescription': nls.localize('decorations.badges', "Controls whether editor file decorations should use badges."), + 'default': false + }, + 'workbench.editor.decorations.colors': { + 'type': 'boolean', + 'markdownDescription': nls.localize('decorations.colors', "Controls whether editor file decorations should use colors."), 'default': false }, 'workbench.editor.labelFormat': { @@ -75,7 +90,7 @@ import { isStandalone } from 'vs/base/browser/browser'; 'type': 'string', 'enum': ['left', 'right', 'off'], 'default': 'right', - 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'editorTabCloseButton' }, "Controls the position of the editor's tabs close buttons, or disables them when set to 'off'. This value is ignored when `#workbench.editor.showTabs#` is `false`.") + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'editorTabCloseButton' }, "Controls the position of the editor's tabs close buttons, or disables them when set to 'off'. This value is ignored when `#workbench.editor.showTabs#` is disabled.") }, 'workbench.editor.tabSizing': { 'type': 'string', @@ -85,7 +100,7 @@ import { isStandalone } from 'vs/base/browser/browser'; nls.localize('workbench.editor.tabSizing.fit', "Always keep tabs large enough to show the full editor label."), nls.localize('workbench.editor.tabSizing.shrink', "Allow tabs to get smaller when the available space is not enough to show all tabs at once.") ], - 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'tabSizing' }, "Controls the sizing of editor tabs. This value is ignored when `#workbench.editor.showTabs#` is `false`.") + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'tabSizing' }, "Controls the sizing of editor tabs. This value is ignored when `#workbench.editor.showTabs#` is disabled.") }, 'workbench.editor.pinnedTabSizing': { 'type': 'string', @@ -96,7 +111,7 @@ import { isStandalone } from 'vs/base/browser/browser'; nls.localize('workbench.editor.pinnedTabSizing.compact', "A pinned tab will show in a compact form with only icon or first letter of the editor name."), nls.localize('workbench.editor.pinnedTabSizing.shrink', "A pinned tab shrinks to a compact fixed size showing parts of the editor name.") ], - 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'pinnedTabSizing' }, "Controls the sizing of pinned editor tabs. Pinned tabs are sorted to the beginning of all opened tabs and typically do not close until unpinned. This value is ignored when `#workbench.editor.showTabs#` is `false`.") + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'pinnedTabSizing' }, "Controls the sizing of pinned editor tabs. Pinned tabs are sorted to the beginning of all opened tabs and typically do not close until unpinned. This value is ignored when `#workbench.editor.showTabs#` is disabled.") }, 'workbench.editor.splitSizing': { 'type': 'string', @@ -130,7 +145,12 @@ import { isStandalone } from 'vs/base/browser/browser'; }, 'workbench.editor.enablePreviewFromQuickOpen': { 'type': 'boolean', - 'description': nls.localize('enablePreviewFromQuickOpen', "Controls whether editors opened from Quick Open show as preview. Preview editors do not keep open and are reused until explicitly set to be kept open (e.g. via double click or editing)."), + 'markdownDescription': nls.localize('enablePreviewFromQuickOpen', "Controls whether editors opened from Quick Open show as preview. Preview editors do not keep open and are reused until explicitly set to be kept open (e.g. via double click or editing). This value is ignored when `#workbench.editor.enablePreview#` is disabled."), + 'default': false + }, + 'workbench.editor.enablePreviewFromCodeNavigation': { + 'type': 'boolean', + 'markdownDescription': nls.localize('enablePreviewFromCodeNavigation', "Controls whether editors remain in preview when a code navigation is started from them. Preview editors do not keep open and are reused until explicitly set to be kept open (e.g. via double click or editing). This value is ignored when `#workbench.editor.enablePreview#` is disabled."), 'default': false }, 'workbench.editor.closeOnFileDelete': { @@ -315,8 +335,8 @@ import { isStandalone } from 'vs/base/browser/browser'; nls.localize('activeFolderLong', "`\${activeFolderLong}`: the full path of the folder the file is contained in (e.g. /Users/Development/myFolder/myFileFolder)."), nls.localize('folderName', "`\${folderName}`: name of the workspace folder the file is contained in (e.g. myFolder)."), nls.localize('folderPath', "`\${folderPath}`: file path of the workspace folder the file is contained in (e.g. /Users/Development/myFolder)."), - nls.localize('rootName', "`\${rootName}`: name of the workspace (e.g. myFolder or myWorkspace)."), - nls.localize('rootPath', "`\${rootPath}`: file path of the workspace (e.g. /Users/Development/myWorkspace)."), + nls.localize('rootName', "`\${rootName}`: name of the opened workspace or folder (e.g. myFolder or myWorkspace)."), + nls.localize('rootPath', "`\${rootPath}`: file path of the opened workspace or folder (e.g. /Users/Development/myWorkspace)."), nls.localize('appName', "`\${appName}`: e.g. VS Code."), nls.localize('remoteName', "`\${remoteName}`: e.g. SSH"), nls.localize('dirty', "`\${dirty}`: a dirty indicator if the active editor is dirty."), @@ -353,7 +373,7 @@ import { isStandalone } from 'vs/base/browser/browser'; 'window.menuBarVisibility': { 'type': 'string', 'enum': ['default', 'visible', 'toggle', 'hidden', 'compact'], - 'enumDescriptions': [ + 'markdownEnumDescriptions': [ nls.localize('window.menuBarVisibility.default', "Menu is only hidden in full screen mode."), nls.localize('window.menuBarVisibility.visible', "Menu is always visible even in full screen mode."), nls.localize('window.menuBarVisibility.toggle', "Menu is hidden but can be displayed via Alt key."), @@ -463,7 +483,7 @@ import { isStandalone } from 'vs/base/browser/browser'; }, 'zenMode.restore': { 'type': 'boolean', - 'default': false, + 'default': true, 'description': nls.localize('zenMode.restore', "Controls whether a window should restore to zen mode if it was exited in zen mode.") }, 'zenMode.silentNotifications': { diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index 2172c094..aedb2a6f 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -8,7 +8,7 @@ import 'vs/workbench/browser/style'; import { localize } from 'vs/nls'; import { Emitter, setGlobalLeakWarningThreshold } from 'vs/base/common/event'; import { runWhenIdle } from 'vs/base/common/async'; -import { getZoomLevel, isFirefox, isSafari, isChrome } from 'vs/base/browser/browser'; +import { getZoomLevel, isFirefox, isSafari, isChrome, getPixelRatio } from 'vs/base/browser/browser'; import { mark } from 'vs/base/common/performance'; import { onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -177,10 +177,17 @@ export class Workbench extends Layout { // Layout Service serviceCollection.set(IWorkbenchLayoutService, this); - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - // NOTE: DO NOT ADD ANY OTHER SERVICE INTO THE COLLECTION HERE. - // CONTRIBUTE IT VIA WORKBENCH.DESKTOP.MAIN.TS AND registerSingleton(). - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // + // NOTE: Please do NOT register services here. Use `registerSingleton()` + // from `workbench.common.main.ts` if the service is shared between + // native and web or `workbench.sandbox.main.ts` if the service + // is native only. + // + // DO NOT add services to `workbench.desktop.main.ts`, always add + // to `workbench.sandbox.main.ts` to support our Electron sandbox + // + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // All Contributed Services const contributedServices = getSingletonServiceDescriptors(); @@ -284,7 +291,7 @@ export class Workbench extends Layout { } } - readFontInfo(BareFontInfo.createFromRawSettings(configurationService.getValue('editor'), getZoomLevel())); + readFontInfo(BareFontInfo.createFromRawSettings(configurationService.getValue('editor'), getZoomLevel(), getPixelRatio())); } private storeFontInfo(storageService: IStorageService): void { @@ -412,10 +419,10 @@ export class Workbench extends Layout { }, 2500); // Telemetry: startup metrics - mark('didStartWorkbench'); + mark('code/didStartWorkbench'); // Perf reporting (devtools) - performance.measure('perf: workbench create & restore', 'didLoadWorkbenchMain', 'didStartWorkbench'); + performance.measure('perf: workbench create & restore', 'code/didLoadWorkbenchMain', 'code/didStartWorkbench'); } } } diff --git a/src/vs/workbench/common/actions.ts b/src/vs/workbench/common/actions.ts index de82ce88..dfe61deb 100644 --- a/src/vs/workbench/common/actions.ts +++ b/src/vs/workbench/common/actions.ts @@ -128,5 +128,6 @@ Registry.add(Extensions.WorkbenchActions, new class implements IWorkbenchActionR export const CATEGORIES = { View: { value: localize('view', "View"), original: 'View' }, Help: { value: localize('help', "Help"), original: 'Help' }, + Preferences: { value: localize('preferences', "Preferences"), original: 'Preferences' }, Developer: { value: localize({ key: 'developer', comment: ['A developer on Code itself or someone diagnosing issues in Code'] }, "Developer"), original: 'Developer' } }; diff --git a/src/vs/workbench/common/composite.ts b/src/vs/workbench/common/composite.ts index 8b48a7f4..d27f59b3 100644 --- a/src/vs/workbench/common/composite.ts +++ b/src/vs/workbench/common/composite.ts @@ -18,6 +18,11 @@ export interface IComposite { */ readonly onDidBlur: Event; + /** + * Returns true if the composite has focus. + */ + hasFocus(): boolean; + /** * Returns the unique identifier of this composite. */ diff --git a/src/vs/workbench/common/dialogs.ts b/src/vs/workbench/common/dialogs.ts index 5de8eac6..5e056158 100644 --- a/src/vs/workbench/common/dialogs.ts +++ b/src/vs/workbench/common/dialogs.ts @@ -18,6 +18,7 @@ export interface IDialogHandle { } export interface IDialogsModel { + readonly onDidShowDialog: Event; readonly dialogs: IDialogViewItem[]; @@ -26,6 +27,7 @@ export interface IDialogsModel { } export class DialogsModel extends Disposable implements IDialogsModel { + readonly dialogs: IDialogViewItem[] = []; private readonly _onDidShowDialog = this._register(new Emitter()); diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index f104149d..88332820 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -74,7 +74,7 @@ export interface IEditorPane extends IComposite { /** * The assigned options of the editor. */ - readonly options: EditorOptions | undefined; + readonly options: IEditorOptions | undefined; /** * The assigned group this editor is showing in. @@ -1215,7 +1215,7 @@ export interface IEditorOpenContext { * An editor is new for a group if it was not part of the group before and * otherwise was already opened in the group and just became the active editor. * - * This hint can e.g. be used to decide wether to restore view state or not. + * This hint can e.g. be used to decide whether to restore view state or not. */ newInGroup?: boolean; } @@ -1257,14 +1257,15 @@ export interface IEditorCloseEvent extends IEditorIdentifier { export type GroupIdentifier = number; export interface IWorkbenchEditorConfiguration { - workbench: { - editor: IEditorPartConfiguration, - iconTheme: string; + workbench?: { + editor?: IEditorPartConfiguration, + iconTheme?: string; }; } interface IEditorPartConfiguration { showTabs?: boolean; + wrapTabs?: boolean; scrollToSwitchTabs?: boolean; highlightModifiedTabs?: boolean; tabCloseButton?: 'left' | 'right' | 'off'; @@ -1275,6 +1276,7 @@ interface IEditorPartConfiguration { showIcons?: boolean; enablePreview?: boolean; enablePreviewFromQuickOpen?: boolean; + enablePreviewFromCodeNavigation?: boolean; closeOnFileDelete?: boolean; openPositioning?: 'left' | 'right' | 'first' | 'last'; openSideBySideDirection?: 'right' | 'down'; @@ -1290,6 +1292,10 @@ interface IEditorPartConfiguration { value?: number; perEditorGroup?: boolean; }; + decorations?: { + badges?: boolean; + colors?: boolean; + } } export interface IEditorPartOptions extends IEditorPartConfiguration { @@ -1328,7 +1334,9 @@ class EditorResourceAccessorImpl { /** * The original URI of an editor is the URI that was used originally to open * the editor and should be used whenever the URI is presented to the user, - * e.g. as a label. + * e.g. as a label together with utility methods such as `ResourceLabel` or + * `ILabelService` that can turn this original URI into the best form for + * presenting. * * In contrast, the canonical URI (#getCanonicalUri) may be different and should * be used whenever the URI is used to e.g. compare with other editors or when diff --git a/src/vs/workbench/common/editor/diffEditorInput.ts b/src/vs/workbench/common/editor/diffEditorInput.ts index 38051db4..1b0eed95 100644 --- a/src/vs/workbench/common/editor/diffEditorInput.ts +++ b/src/vs/workbench/common/editor/diffEditorInput.ts @@ -13,6 +13,7 @@ import { dirname } from 'vs/base/common/resources'; import { ILabelService } from 'vs/platform/label/common/label'; import { IFileService } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; +import { withNullAsUndefined } from 'vs/base/common/types'; /** * The base editor input for the diff editor. It is made up of two editor inputs, the original version @@ -110,21 +111,18 @@ export class DiffEditorInput extends SideBySideEditorInput { private async createModel(): Promise { // Join resolve call over two inputs and build diff editor model - const models = await Promise.all([ + const [originalEditorModel, modifiedEditorModel] = await Promise.all([ this.originalInput.resolve(), this.modifiedInput.resolve() ]); - const originalEditorModel = models[0]; - const modifiedEditorModel = models[1]; - // If both are text models, return textdiffeditor model if (modifiedEditorModel instanceof BaseTextEditorModel && originalEditorModel instanceof BaseTextEditorModel) { return new TextDiffEditorModel(originalEditorModel, modifiedEditorModel); } // Otherwise return normal diff model - return new DiffEditorModel(originalEditorModel, modifiedEditorModel); + return new DiffEditorModel(withNullAsUndefined(originalEditorModel), withNullAsUndefined(modifiedEditorModel)); } matches(otherInput: unknown): boolean { diff --git a/src/vs/workbench/common/editor/diffEditorModel.ts b/src/vs/workbench/common/editor/diffEditorModel.ts index dfa51d62..5550bf99 100644 --- a/src/vs/workbench/common/editor/diffEditorModel.ts +++ b/src/vs/workbench/common/editor/diffEditorModel.ts @@ -12,13 +12,13 @@ import { IEditorModel } from 'vs/platform/editor/common/editor'; */ export class DiffEditorModel extends EditorModel { - protected readonly _originalModel: IEditorModel | null; - get originalModel(): IEditorModel | null { return this._originalModel; } + protected readonly _originalModel: IEditorModel | undefined; + get originalModel(): IEditorModel | undefined { return this._originalModel; } - protected readonly _modifiedModel: IEditorModel | null; - get modifiedModel(): IEditorModel | null { return this._modifiedModel; } + protected readonly _modifiedModel: IEditorModel | undefined; + get modifiedModel(): IEditorModel | undefined { return this._modifiedModel; } - constructor(originalModel: IEditorModel | null, modifiedModel: IEditorModel | null) { + constructor(originalModel: IEditorModel | undefined, modifiedModel: IEditorModel | undefined) { super(); this._originalModel = originalModel; @@ -28,7 +28,7 @@ export class DiffEditorModel extends EditorModel { async load(): Promise { await Promise.all([ this._originalModel?.load(), - this._modifiedModel?.load(), + this._modifiedModel?.load() ]); return this; diff --git a/src/vs/workbench/common/editor/editorGroup.ts b/src/vs/workbench/common/editor/editorGroup.ts index 8d7410ac..d1580899 100644 --- a/src/vs/workbench/common/editor/editorGroup.ts +++ b/src/vs/workbench/common/editor/editorGroup.ts @@ -724,12 +724,19 @@ export class EditorGroup extends Disposable { clone(): EditorGroup { const group = this.instantiationService.createInstance(EditorGroup, undefined); + + // Copy over group properties group.editors = this.editors.slice(0); group.mru = this.mru.slice(0); group.preview = this.preview; group.active = this.active; group.sticky = this.sticky; + // Ensure to register listeners for each editor + for (const editor of group.editors) { + group.registerEditorListeners(editor); + } + return group; } diff --git a/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts index a72d2652..0167182c 100644 --- a/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -97,7 +97,7 @@ export class ResourceEditorInput extends AbstractTextResourceEditorInput impleme ref.dispose(); this.modelReference = undefined; - throw new Error(`Unexpected model for ResourcEditorInput: ${this.resource}`); + throw new Error(`Unexpected model for ResourceEditorInput: ${this.resource}`); } this.cachedModel = model; diff --git a/src/vs/workbench/common/editor/resourceEditorModel.ts b/src/vs/workbench/common/editor/resourceEditorModel.ts index e8e58c3f..56f7aefe 100644 --- a/src/vs/workbench/common/editor/resourceEditorModel.ts +++ b/src/vs/workbench/common/editor/resourceEditorModel.ts @@ -23,7 +23,7 @@ export class ResourceEditorModel extends BaseTextEditorModel { dispose(): void { - // TODO@Joao: force this class to dispose the underlying model + // force this class to dispose the underlying model if (this.textEditorModelHandle) { this.modelService.destroyModel(this.textEditorModelHandle); } diff --git a/src/vs/workbench/common/editor/textDiffEditorModel.ts b/src/vs/workbench/common/editor/textDiffEditorModel.ts index fcb97b42..98528b2e 100644 --- a/src/vs/workbench/common/editor/textDiffEditorModel.ts +++ b/src/vs/workbench/common/editor/textDiffEditorModel.ts @@ -14,14 +14,14 @@ import { DiffEditorModel } from 'vs/workbench/common/editor/diffEditorModel'; */ export class TextDiffEditorModel extends DiffEditorModel { - protected readonly _originalModel: BaseTextEditorModel | null; - get originalModel(): BaseTextEditorModel | null { return this._originalModel; } + protected readonly _originalModel: BaseTextEditorModel | undefined; + get originalModel(): BaseTextEditorModel | undefined { return this._originalModel; } - protected readonly _modifiedModel: BaseTextEditorModel | null; - get modifiedModel(): BaseTextEditorModel | null { return this._modifiedModel; } + protected readonly _modifiedModel: BaseTextEditorModel | undefined; + get modifiedModel(): BaseTextEditorModel | undefined { return this._modifiedModel; } - private _textDiffEditorModel: IDiffEditorModel | null = null; - get textDiffEditorModel(): IDiffEditorModel | null { return this._textDiffEditorModel; } + private _textDiffEditorModel: IDiffEditorModel | undefined = undefined; + get textDiffEditorModel(): IDiffEditorModel | undefined { return this._textDiffEditorModel; } constructor(originalModel: BaseTextEditorModel, modifiedModel: BaseTextEditorModel) { super(originalModel, modifiedModel); @@ -73,7 +73,7 @@ export class TextDiffEditorModel extends DiffEditorModel { // inside. We never created the two models (original and modified) so we can not dispose // them without sideeffects. Rather rely on the models getting disposed when their related // inputs get disposed from the diffEditorInput. - this._textDiffEditorModel = null; + this._textDiffEditorModel = undefined; super.dispose(); } diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts index f730d008..3225ea5a 100644 --- a/src/vs/workbench/common/editor/textEditorModel.ts +++ b/src/vs/workbench/common/editor/textEditorModel.ts @@ -18,7 +18,7 @@ import { withUndefinedAsNull } from 'vs/base/common/types'; */ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel, IModeSupport { - protected textEditorModelHandle: URI | null = null; + protected textEditorModelHandle: URI | undefined = undefined; private createdEditorModel: boolean | undefined; @@ -52,7 +52,7 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel private registerModelDisposeListener(model: ITextModel): void { this.modelDisposeListener.value = model.onWillDispose(() => { - this.textEditorModelHandle = null; // make sure we do not dispose code editor model again + this.textEditorModelHandle = undefined; // make sure we do not dispose code editor model again this.dispose(); }); } @@ -178,7 +178,7 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel this.modelService.destroyModel(this.textEditorModelHandle); } - this.textEditorModelHandle = null; + this.textEditorModelHandle = undefined; this.createdEditorModel = false; super.dispose(); diff --git a/src/vs/workbench/common/editor/textResourceEditorInput.ts b/src/vs/workbench/common/editor/textResourceEditorInput.ts index e55b5567..b738dca4 100644 --- a/src/vs/workbench/common/editor/textResourceEditorInput.ts +++ b/src/vs/workbench/common/editor/textResourceEditorInput.ts @@ -199,14 +199,14 @@ export abstract class AbstractTextResourceEditorInput extends EditorInput implem } // Normal save - return this.doSave(group, options, false); + return this.doSave(options, false); } saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { - return this.doSave(group, options, true); + return this.doSave(options, true); } - private async doSave(group: GroupIdentifier, options: ISaveOptions | undefined, saveAs: boolean): Promise { + private async doSave(options: ISaveOptions | undefined, saveAs: boolean): Promise { // Save / Save As let target: URI | undefined; @@ -220,8 +220,13 @@ export abstract class AbstractTextResourceEditorInput extends EditorInput implem return undefined; // save cancelled } - // If the target is a different resource, return with a new editor input - if (!isEqual(target, this.preferredResource)) { + // If this save operation results in a new editor, either + // because it was saved to disk (e.g. from untitled) or + // through an explicit "Save As", make sure to replace it. + if ( + target.scheme !== this.resource.scheme || + (saveAs && !isEqual(target, this.preferredResource)) + ) { return this.editorService.createEditorInput({ resource: target }); } diff --git a/src/vs/workbench/common/panel.ts b/src/vs/workbench/common/panel.ts index 7b836be9..555e85dc 100644 --- a/src/vs/workbench/common/panel.ts +++ b/src/vs/workbench/common/panel.ts @@ -9,5 +9,7 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export const ActivePanelContext = new RawContextKey('activePanel', ''); export const PanelFocusContext = new RawContextKey('panelFocus', false); export const PanelPositionContext = new RawContextKey('panelPosition', 'bottom'); +export const PanelVisibleContext = new RawContextKey('panelVisible', false); +export const PanelMaximizedContext = new RawContextKey('panelMaximized', false); export interface IPanel extends IComposite { } diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 53f29efb..317cfd2f 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -339,7 +339,7 @@ export const STATUS_BAR_FOREGROUND = registerColor('statusBar.foreground', { dark: '#FFFFFF', light: '#FFFFFF', hc: '#FFFFFF' -}, nls.localize('statusBarForeground', "Status bar foreground color when a workspace is opened. The status bar is shown in the bottom of the window.")); +}, nls.localize('statusBarForeground', "Status bar foreground color when a workspace or folder is opened. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_NO_FOLDER_FOREGROUND = registerColor('statusBar.noFolderForeground', { dark: STATUS_BAR_FOREGROUND, @@ -351,7 +351,7 @@ export const STATUS_BAR_BACKGROUND = registerColor('statusBar.background', { dark: '#007ACC', light: '#007ACC', hc: null -}, nls.localize('statusBarBackground', "Status bar background color when a workspace is opened. The status bar is shown in the bottom of the window.")); +}, nls.localize('statusBarBackground', "Status bar background color when a workspace or folder is opened. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_NO_FOLDER_BACKGROUND = registerColor('statusBar.noFolderBackground', { dark: '#68217A', diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 0744cecf..7bbafb80 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -27,9 +27,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { mixin } from 'vs/base/common/objects'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; - -export const TEST_VIEW_CONTAINER_ID = 'workbench.view.extension.test'; -export const testViewIcon = registerIcon('test-view-icon', Codicon.beaker, localize('testViewIcon', 'View icon of the test view.')); +import { CancellationToken } from 'vs/base/common/cancellation'; export const defaultViewIcon = registerIcon('default-view-icon', Codicon.window, localize('defaultViewIcon', 'Default view icon.')); @@ -38,11 +36,18 @@ export namespace Extensions { export const ViewsRegistry = 'workbench.registry.view'; } -export enum ViewContainerLocation { +export const enum ViewContainerLocation { Sidebar, Panel } +export function ViewContainerLocationToString(viewContainerLocation: ViewContainerLocation) { + switch (viewContainerLocation) { + case ViewContainerLocation.Sidebar: return 'sidebar'; + case ViewContainerLocation.Panel: return 'panel'; + } +} + export interface IViewContainerDescriptor { readonly id: string; @@ -256,6 +261,8 @@ export interface IAddedViewDescriptorState { export interface IViewContainerModel { + readonly viewContainer: ViewContainer; + readonly title: string; readonly icon: ThemeIcon | URI | undefined; readonly onDidChangeContainerInfo: Event<{ title?: boolean, icon?: boolean }>; @@ -505,7 +512,7 @@ export interface IViewsService { * View Contexts */ export const FocusedViewContext = new RawContextKey('focusedView', ''); -export function getVisbileViewContextKey(viewId: string): string { return `${viewId}.visible`; } +export function getVisbileViewContextKey(viewId: string): string { return `view.${viewId}.visible`; } export const IViewDescriptorService = createDecorator('viewDescriptorService'); @@ -684,21 +691,24 @@ export class ResolvableTreeItem implements ITreeItem { command?: Command; children?: ITreeItem[]; accessibilityInformation?: IAccessibilityInformation; - resolve: () => Promise; + resolve: (token: CancellationToken) => Promise; private resolved: boolean = false; private _hasResolve: boolean = false; - constructor(treeItem: ITreeItem, resolve?: (() => Promise)) { + constructor(treeItem: ITreeItem, resolve?: ((token: CancellationToken) => Promise)) { mixin(this, treeItem); this._hasResolve = !!resolve; - this.resolve = async () => { + this.resolve = async (token: CancellationToken) => { if (resolve && !this.resolved) { - const resolvedItem = await resolve(); + const resolvedItem = await resolve(token); if (resolvedItem) { - // Resolvable elements. Currently only tooltip. - this.tooltip = resolvedItem.tooltip; + // Resolvable elements. Currently tooltip and command. + this.tooltip = this.tooltip ?? resolvedItem.tooltip; + this.command = this.command ?? resolvedItem.command; } } - this.resolved = true; + if (!token.isCancellationRequested) { + this.resolved = true; + } }; } get hasResolve(): boolean { @@ -756,5 +766,6 @@ export interface IViewPaneContainer { getActionViewItem(action: IAction): IActionViewItem | undefined; getActionsContext(): unknown; getView(viewId: string): IView | undefined; + toggleViewVisibility(viewId: string): void; saveState(): void; } diff --git a/src/vs/workbench/contrib/backup/common/backupTracker.ts b/src/vs/workbench/contrib/backup/common/backupTracker.ts index d8f35fc9..f9767e53 100644 --- a/src/vs/workbench/contrib/backup/common/backupTracker.ts +++ b/src/vs/workbench/contrib/backup/common/backupTracker.ts @@ -18,7 +18,7 @@ export abstract class BackupTracker extends Disposable { private readonly mapWorkingCopyToContentVersion = new Map(); // A map of scheduled pending backups for working copies - private readonly pendingBackups = new Map(); + protected readonly pendingBackups = new Map(); constructor( protected readonly backupFileService: IBackupFileService, @@ -43,7 +43,7 @@ export abstract class BackupTracker extends Disposable { this._register(this.workingCopyService.onDidChangeContent(workingCopy => this.onDidChangeContent(workingCopy))); // Lifecycle (handled in subclasses) - this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason))); + this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason), 'veto.backups')); } private onDidRegister(workingCopy: IWorkingCopy): void { diff --git a/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts b/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts index d2c4f92f..c4a4e33a 100644 --- a/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts +++ b/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts @@ -9,7 +9,7 @@ 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/workbench/services/lifecycle/common/lifecycle'; -import { ConfirmResult, IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ConfirmResult, IFileDialogService, IDialogService, getFileNamesMessage } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { isMacintosh } from 'vs/base/common/platform'; @@ -20,7 +20,9 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { SaveReason } from 'vs/workbench/common/editor'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { raceCancellation } from 'vs/base/common/async'; export class NativeBackupTracker extends BackupTracker implements IWorkbenchContribution { @@ -47,7 +49,8 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont @INativeHostService private readonly nativeHostService: INativeHostService, @ILogService logService: ILogService, @IEditorService private readonly editorService: IEditorService, - @IEnvironmentService private readonly environmentService: IEnvironmentService + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IProgressService private readonly progressService: IProgressService ) { super(backupFileService, workingCopyService, logService, lifecycleService); } @@ -121,7 +124,7 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont // we ran a backup but received an error that we show to the user if (backupError) { - this.showErrorDialog(localize('backupTrackerBackupFailed', "One or more dirty editors could not be saved to the back up location."), backupError); + this.showErrorDialog(localize('backupTrackerBackupFailed', "The following dirty editors could not be saved to the back up location."), workingCopies, backupError); return true; // veto (the backup failed) } @@ -131,14 +134,21 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont try { return await this.confirmBeforeShutdown(workingCopies.filter(workingCopy => !backups.includes(workingCopy))); } catch (error) { - this.showErrorDialog(localize('backupTrackerConfirmFailed', "One or more dirty editors could not be saved or reverted."), error); + this.showErrorDialog(localize('backupTrackerConfirmFailed', "The following dirty editors could not be saved or reverted."), workingCopies, error); return true; // veto (save or revert failed) } } - private showErrorDialog(msg: string, error?: Error): void { - this.dialogService.show(Severity.Error, msg, [localize('ok', 'OK')], { detail: localize('backupErrorDetails', "Try saving or reverting the dirty editors first and then try again.") }); + private showErrorDialog(msg: string, workingCopies: readonly IWorkingCopy[], error?: Error): void { + const dirtyEditors = workingCopies.filter(workingCopy => workingCopy.isDirty()); + + const advice = localize('backupErrorDetails', "Try saving or reverting the dirty editors first and then try again."); + const detail = dirtyEditors.length + ? getFileNamesMessage(dirtyEditors.map(x => x.name)) + '\n' + advice + : advice; + + this.dialogService.show(Severity.Error, msg, [localize('ok', 'OK')], { detail }); this.logService.error(error ? `[backup tracker] ${msg}: ${error}` : `[backup tracker] ${msg}`); } @@ -234,9 +244,9 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont return true; // veto (user canceled) } - private async doSaveAllBeforeShutdown(workingCopies: IWorkingCopy[], reason: SaveReason): Promise; - private async doSaveAllBeforeShutdown(includeUntitled: boolean, reason: SaveReason): Promise; - private async doSaveAllBeforeShutdown(arg1: IWorkingCopy[] | boolean, reason: SaveReason): Promise { + private doSaveAllBeforeShutdown(workingCopies: IWorkingCopy[], reason: SaveReason): Promise; + private doSaveAllBeforeShutdown(includeUntitled: boolean, reason: SaveReason): Promise; + private doSaveAllBeforeShutdown(arg1: IWorkingCopy[] | boolean, reason: SaveReason): Promise { const workingCopies = Array.isArray(arg1) ? arg1 : this.workingCopyService.dirtyWorkingCopies.filter(workingCopy => { if (arg1 === false && (workingCopy.capabilities & WorkingCopyCapabilities.Untitled)) { return false; // skip untitled unless explicitly included @@ -245,21 +255,34 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont return true; }); - // Skip save participants on shutdown for performance reasons - const saveOptions = { skipSaveParticipants: true, reason }; + const cts = new CancellationTokenSource(); + return this.progressService.withProgress({ + location: ProgressLocation.Notification, + cancellable: true, // for https://github.com/microsoft/vscode/issues/112278 + delay: 800, // delay notification so that it only appears when saving takes a long time + title: localize('saveBeforeShutdown', "Waiting for dirty editors to save...") + }, () => { + const saveAllPromise = (async () => { - // First save through the editor service if we save all to benefit - // from some extras like switching to untitled dirty editors before saving. - let result: boolean | undefined = undefined; - if (typeof arg1 === 'boolean' || workingCopies.length === this.workingCopyService.dirtyCount) { - result = await this.editorService.saveAll({ includeUntitled: typeof arg1 === 'boolean' ? arg1 : true, ...saveOptions }); - } + // Skip save participants on shutdown for performance reasons + const saveOptions = { skipSaveParticipants: true, reason }; - // If we still have dirty working copies, save those directly - // unless the save was not successful (e.g. cancelled) - if (result !== false) { - await Promise.all(workingCopies.map(workingCopy => workingCopy.isDirty() ? workingCopy.save(saveOptions) : true)); - } + // First save through the editor service if we save all to benefit + // from some extras like switching to untitled dirty editors before saving. + let result: boolean | undefined = undefined; + if (typeof arg1 === 'boolean' || workingCopies.length === this.workingCopyService.dirtyCount) { + result = await this.editorService.saveAll({ includeUntitled: typeof arg1 === 'boolean' ? arg1 : true, ...saveOptions }); + } + + // If we still have dirty working copies, save those directly + // unless the save was not successful (e.g. cancelled) + if (result !== false) { + await Promise.all(workingCopies.map(workingCopy => workingCopy.isDirty() ? workingCopy.save(saveOptions) : true)); + } + })(); + + return raceCancellation(saveAllPromise, cts.token); + }, () => cts.dispose(true)); } private async doRevertAllBeforeShutdown(workingCopies: IWorkingCopy[]): Promise { diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts b/src/vs/workbench/contrib/backup/test/browser/backupRestorer.test.ts similarity index 56% rename from src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts rename to src/vs/workbench/contrib/backup/test/browser/backupRestorer.test.ts index dfe8325a..1835bff5 100644 --- a/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts +++ b/src/vs/workbench/contrib/backup/test/browser/backupRestorer.test.ts @@ -4,46 +4,20 @@ *--------------------------------------------------------------------------------------------*/ 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 { isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; -import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { DefaultEndOfLine } from 'vs/editor/common/model'; -import { hashPath } from 'vs/workbench/services/backup/electron-browser/backupFileService'; -import { NativeBackupTracker } from 'vs/workbench/contrib/backup/electron-sandbox/backupTracker'; -import { workbenchInstantiationService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; -import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; 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 { Schemas } from 'vs/base/common/network'; import { isEqual } from 'vs/base/common/resources'; -import { TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; +import { InMemoryTestBackupFileService, TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { BackupRestorer } from 'vs/workbench/contrib/backup/common/backupRestorer'; - -const userdataDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backuprestorer'); -const backupHome = path.join(userdataDir, 'Backups'); -const workspacesJsonPath = path.join(backupHome, 'workspaces.json'); - -const workspaceResource = URI.file(platform.isWindows ? 'c:\\workspace' : '/workspace'); -const workspaceBackupPath = path.join(backupHome, hashPath(workspaceResource)); -const fooFile = URI.file(platform.isWindows ? 'c:\\Foo' : '/Foo'); -const barFile = URI.file(platform.isWindows ? 'c:\\Bar' : '/Bar'); -const untitledFile1 = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); -const untitledFile2 = URI.from({ scheme: Schemas.untitled, path: 'Untitled-2' }); +import { BrowserBackupTracker } from 'vs/workbench/contrib/backup/browser/backupTracker'; class TestBackupRestorer extends BackupRestorer { async doRestoreBackups(): Promise { @@ -54,38 +28,13 @@ class TestBackupRestorer extends BackupRestorer { suite('BackupRestorer', () => { let accessor: TestServiceAccessor; - 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); - }); + const fooFile = URI.file(isWindows ? 'c:\\Foo' : '/Foo'); + const barFile = URI.file(isWindows ? 'c:\\Bar' : '/Bar'); + const untitledFile1 = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); + const untitledFile2 = URI.from({ scheme: Schemas.untitled, path: 'Untitled-2' }); test('Restore backups', async function () { - this.timeout(20000); - - const backupFileService = new NodeTestBackupFileService(workspaceBackupPath); + const backupFileService = new InMemoryTestBackupFileService(); const instantiationService = workbenchInstantiationService(); instantiationService.stub(IBackupFileService, backupFileService); @@ -102,18 +51,18 @@ suite('BackupRestorer', () => { await part.whenRestored; - const tracker = instantiationService.createInstance(NativeBackupTracker); + const tracker = instantiationService.createInstance(BrowserBackupTracker); const restorer = instantiationService.createInstance(TestBackupRestorer); // Backup 2 normal files and 2 untitled file - await backupFileService.backup(untitledFile1, createTextBufferFactory('untitled-1').create(DefaultEndOfLine.LF).createSnapshot(false)); - await backupFileService.backup(untitledFile2, createTextBufferFactory('untitled-2').create(DefaultEndOfLine.LF).createSnapshot(false)); - await backupFileService.backup(fooFile, createTextBufferFactory('fooFile').create(DefaultEndOfLine.LF).createSnapshot(false)); - await backupFileService.backup(barFile, createTextBufferFactory('barFile').create(DefaultEndOfLine.LF).createSnapshot(false)); + await backupFileService.backup(untitledFile1, createTextBufferFactory('untitled-1').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); + await backupFileService.backup(untitledFile2, createTextBufferFactory('untitled-2').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); + await backupFileService.backup(fooFile, createTextBufferFactory('fooFile').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); + await backupFileService.backup(barFile, createTextBufferFactory('barFile').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); // Verify backups restored and opened as dirty await restorer.doRestoreBackups(); - assert.equal(editorService.count, 4); + assert.strictEqual(editorService.count, 4); assert.ok(editorService.editors.every(editor => editor.isDirty())); let counter = 0; @@ -152,7 +101,7 @@ suite('BackupRestorer', () => { } } - assert.equal(counter, 4); + assert.strictEqual(counter, 4); part.dispose(); tracker.dispose(); diff --git a/src/vs/workbench/contrib/backup/test/browser/backupTracker.test.ts b/src/vs/workbench/contrib/backup/test/browser/backupTracker.test.ts new file mode 100644 index 00000000..a75a319c --- /dev/null +++ b/src/vs/workbench/contrib/backup/test/browser/backupTracker.test.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { URI } from 'vs/base/common/uri'; +import { 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 { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; +import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { toResource } from 'vs/base/test/common/utils'; +import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IWorkingCopyBackup, IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; +import { InMemoryTestBackupFileService, TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { timeout } from 'vs/base/common/async'; +import { BrowserBackupTracker } from 'vs/workbench/contrib/backup/browser/backupTracker'; + +class TestBackupTracker extends BrowserBackupTracker { + + constructor( + @IBackupFileService backupFileService: IBackupFileService, + @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + @ILifecycleService lifecycleService: ILifecycleService, + @ILogService logService: ILogService, + ) { + super(backupFileService, filesConfigurationService, workingCopyService, lifecycleService, logService); + } + + protected getBackupScheduleDelay(): number { + return 10; // Reduce timeout for tests + } +} + +suite('BackupTracker (browser)', function () { + let accessor: TestServiceAccessor; + + async function createTracker(): Promise<{ accessor: TestServiceAccessor, part: EditorPart, tracker: BackupTracker, backupFileService: InMemoryTestBackupFileService, instantiationService: IInstantiationService, cleanup: () => void }> { + const backupFileService = new InMemoryTestBackupFileService(); + 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(TestServiceAccessor); + + await part.whenRestored; + + const tracker = instantiationService.createInstance(TestBackupTracker); + + const cleanup = () => { + part.dispose(); + tracker.dispose(); + }; + + return { accessor, part, tracker, backupFileService, instantiationService, cleanup }; + } + + async function untitledBackupTest(untitled: IUntitledTextResourceEditorInput = {}): Promise { + const { accessor, cleanup, backupFileService } = await createTracker(); + + const untitledEditor = (await accessor.editorService.openEditor(untitled))?.input as UntitledTextEditorInput; + + const untitledModel = await untitledEditor.resolve(); + + if (!untitled?.contents) { + untitledModel.textEditorModel.setValue('Super Good'); + } + + await backupFileService.joinBackupResource(); + + assert.strictEqual(backupFileService.hasBackupSync(untitledEditor.resource), true); + + untitledModel.dispose(); + + await backupFileService.joinDiscardBackup(); + + assert.strictEqual(backupFileService.hasBackupSync(untitledEditor.resource), false); + + cleanup(); + } + + test('Track backups (untitled)', function () { + return untitledBackupTest(); + }); + + test('Track backups (untitled with initial contents)', function () { + return untitledBackupTest({ contents: 'Foo Bar' }); + }); + + test('Track backups (custom)', async function () { + const { accessor, cleanup, backupFileService } = await createTracker(); + + class TestBackupWorkingCopy extends TestWorkingCopy { + + backupDelay = 0; + + constructor(resource: URI) { + super(resource); + + accessor.workingCopyService.registerWorkingCopy(this); + } + + async backup(token: CancellationToken): Promise { + await timeout(this.backupDelay); + + return {}; + } + } + + const resource = toResource.call(this, '/path/custom.txt'); + const customWorkingCopy = new TestBackupWorkingCopy(resource); + + // Normal + customWorkingCopy.setDirty(true); + await backupFileService.joinBackupResource(); + assert.strictEqual(backupFileService.hasBackupSync(resource), true); + + customWorkingCopy.setDirty(false); + customWorkingCopy.setDirty(true); + await backupFileService.joinBackupResource(); + assert.strictEqual(backupFileService.hasBackupSync(resource), true); + + customWorkingCopy.setDirty(false); + await backupFileService.joinDiscardBackup(); + assert.strictEqual(backupFileService.hasBackupSync(resource), false); + + // Cancellation + customWorkingCopy.setDirty(true); + await timeout(0); + customWorkingCopy.setDirty(false); + await backupFileService.joinDiscardBackup(); + assert.strictEqual(backupFileService.hasBackupSync(resource), false); + + customWorkingCopy.dispose(); + await cleanup(); + }); +}); diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts index efb89faf..01050783 100644 --- a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts +++ b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts @@ -9,7 +9,7 @@ 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 { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { hashPath } from 'vs/workbench/services/backup/electron-browser/backupFileService'; import { NativeBackupTracker } from 'vs/workbench/contrib/backup/electron-sandbox/backupTracker'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; @@ -18,7 +18,7 @@ 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, IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; +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'; @@ -28,7 +28,7 @@ import { NodeTestBackupFileService } from 'vs/workbench/services/backup/test/ele 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 { IWorkingCopyBackup, IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ILogService } from 'vs/platform/log/common/log'; import { HotExitConfiguration } from 'vs/platform/files/common/files'; import { ShutdownReason, ILifecycleService, BeforeShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -38,24 +38,14 @@ import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker'; import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestFilesConfigurationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { timeout } from 'vs/base/common/async'; import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; - -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)); +import { IProgressService } from 'vs/platform/progress/common/progress'; class TestBackupTracker extends NativeBackupTracker { @@ -70,14 +60,22 @@ class TestBackupTracker extends NativeBackupTracker { @INativeHostService nativeHostService: INativeHostService, @ILogService logService: ILogService, @IEditorService editorService: IEditorService, - @IEnvironmentService environmentService: IEnvironmentService + @IEnvironmentService environmentService: IEnvironmentService, + @IProgressService progressService: IProgressService ) { - super(backupFileService, filesConfigurationService, workingCopyService, lifecycleService, fileDialogService, dialogService, contextService, nativeHostService, logService, editorService, environmentService); + super(backupFileService, filesConfigurationService, workingCopyService, lifecycleService, fileDialogService, dialogService, contextService, nativeHostService, logService, editorService, environmentService, progressService); } protected getBackupScheduleDelay(): number { return 10; // Reduce timeout for tests } + + dispose() { + super.dispose(); + for (const [_, disposable] of this.pendingBackups) { + disposable.dispose(); + } + } } class BeforeShutdownEventImpl implements BeforeShutdownEvent { @@ -90,11 +88,22 @@ class BeforeShutdownEventImpl implements BeforeShutdownEvent { } } -suite('BackupTracker', () => { +flakySuite('BackupTracker (native)', function () { + let testDir: string; + let backupHome: string; + let workspaceBackupPath: string; + let accessor: TestServiceAccessor; let disposables: IDisposable[] = []; setup(async () => { + testDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backuprestorer'); + backupHome = path.join(testDir, 'Backups'); + const workspacesJsonPath = path.join(backupHome, 'workspaces.json'); + + const workspaceResource = URI.file(platform.isWindows ? 'c:\\workspace' : '/workspace'); + workspaceBackupPath = path.join(backupHome, hashPath(workspaceResource)); + const instantiationService = workbenchInstantiationService(); accessor = instantiationService.createInstance(TestServiceAccessor); @@ -107,8 +116,6 @@ suite('BackupTracker', () => { [new SyncDescriptor(FileEditorInput)] )); - // Delete any existing backups completely and then re-create it. - await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); await pfs.mkdirp(backupHome); await pfs.mkdirp(workspaceBackupPath); @@ -121,11 +128,11 @@ suite('BackupTracker', () => { (accessor.textFileService.files).dispose(); - return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); + return pfs.rimraf(testDir); }); - async function createTracker(autoSaveEnabled = false): Promise<[TestServiceAccessor, EditorPart, BackupTracker, IInstantiationService]> { - const backupFileService = new NodeTestBackupFileService(workspaceBackupPath); + async function createTracker(autoSaveEnabled = false): Promise<{ accessor: TestServiceAccessor, part: EditorPart, tracker: BackupTracker, instantiationService: IInstantiationService, cleanup: () => Promise }> { + const backupFileService = new NodeTestBackupFileService(testDir, workspaceBackupPath); const instantiationService = workbenchInstantiationService(); instantiationService.stub(IBackupFileService, backupFileService); @@ -155,50 +162,19 @@ suite('BackupTracker', () => { const tracker = instantiationService.createInstance(TestBackupTracker); - return [accessor, part, tracker, instantiationService]; + const cleanup = async () => { + // File changes could also schedule some backup operations so we need to wait for them before finishing the test + await accessor.backupFileService.waitForAllBackups(); + + part.dispose(); + tracker.dispose(); + }; + + return { accessor, part, tracker, instantiationService, cleanup }; } - async function untitledBackupTest(untitled: IUntitledTextResourceEditorInput = {}): Promise { - const [accessor, part, tracker] = await createTracker(); - - const untitledEditor = (await accessor.editorService.openEditor(untitled))?.input as UntitledTextEditorInput; - - const untitledModel = await untitledEditor.resolve(); - - if (!untitled?.contents) { - untitledModel.textEditorModel.setValue('Super Good'); - } - - await accessor.backupFileService.joinBackupResource(); - - assert.equal(accessor.backupFileService.hasBackupSync(untitledEditor.resource), true); - - untitledModel.dispose(); - - await accessor.backupFileService.joinDiscardBackup(); - - assert.equal(accessor.backupFileService.hasBackupSync(untitledEditor.resource), false); - - part.dispose(); - tracker.dispose(); - } - - test('Track backups (untitled)', function () { - this.timeout(20000); - - return untitledBackupTest(); - }); - - test('Track backups (untitled with initial contents)', function () { - this.timeout(20000); - - return untitledBackupTest({ contents: 'Foo Bar' }); - }); - test('Track backups (file)', async function () { - this.timeout(20000); - - const [accessor, part, tracker] = await createTracker(); + const { accessor, cleanup } = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { pinned: true } }); @@ -208,69 +184,19 @@ suite('BackupTracker', () => { await accessor.backupFileService.joinBackupResource(); - assert.equal(accessor.backupFileService.hasBackupSync(resource), true); + assert.strictEqual(accessor.backupFileService.hasBackupSync(resource), true); fileModel?.dispose(); await accessor.backupFileService.joinDiscardBackup(); - assert.equal(accessor.backupFileService.hasBackupSync(resource), false); + assert.strictEqual(accessor.backupFileService.hasBackupSync(resource), false); - part.dispose(); - tracker.dispose(); - }); - - test('Track backups (custom)', async function () { - const [accessor, part, tracker] = await createTracker(); - - class TestBackupWorkingCopy extends TestWorkingCopy { - - backupDelay = 0; - - constructor(resource: URI) { - super(resource); - - accessor.workingCopyService.registerWorkingCopy(this); - } - - async backup(token: CancellationToken): Promise { - await timeout(this.backupDelay); - - return {}; - } - } - - const resource = toResource.call(this, '/path/custom.txt'); - const customWorkingCopy = new TestBackupWorkingCopy(resource); - - // Normal - customWorkingCopy.setDirty(true); - await accessor.backupFileService.joinBackupResource(); - assert.equal(accessor.backupFileService.hasBackupSync(resource), true); - - customWorkingCopy.setDirty(false); - customWorkingCopy.setDirty(true); - await accessor.backupFileService.joinBackupResource(); - assert.equal(accessor.backupFileService.hasBackupSync(resource), true); - - customWorkingCopy.setDirty(false); - await accessor.backupFileService.joinDiscardBackup(); - assert.equal(accessor.backupFileService.hasBackupSync(resource), false); - - // Cancellation - customWorkingCopy.setDirty(true); - await timeout(0); - customWorkingCopy.setDirty(false); - await accessor.backupFileService.joinDiscardBackup(); - assert.equal(accessor.backupFileService.hasBackupSync(resource), false); - - customWorkingCopy.dispose(); - part.dispose(); - tracker.dispose(); + await cleanup(); }); test('onWillShutdown - no veto if no dirty files', async function () { - const [accessor, part, tracker] = await createTracker(); + const { accessor, cleanup } = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { pinned: true } }); @@ -281,12 +207,11 @@ suite('BackupTracker', () => { const veto = await event.value; assert.ok(!veto); - part.dispose(); - tracker.dispose(); + await cleanup(); }); test('onWillShutdown - veto if user cancels (hot.exit: off)', async function () { - const [accessor, part, tracker] = await createTracker(); + const { accessor, cleanup } = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { pinned: true } }); @@ -298,7 +223,7 @@ suite('BackupTracker', () => { await model?.load(); model?.textEditorModel?.setValue('foo'); - assert.equal(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); const event = new BeforeShutdownEventImpl(); accessor.lifecycleService.fireWillShutdown(event); @@ -306,12 +231,11 @@ suite('BackupTracker', () => { const veto = await event.value; assert.ok(veto); - part.dispose(); - tracker.dispose(); + await cleanup(); }); test('onWillShutdown - no veto if auto save is on', async function () { - const [accessor, part, tracker] = await createTracker(true /* auto save enabled */); + const { accessor, cleanup } = await createTracker(true /* auto save enabled */); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { pinned: true } }); @@ -320,7 +244,7 @@ suite('BackupTracker', () => { await model?.load(); model?.textEditorModel?.setValue('foo'); - assert.equal(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); const event = new BeforeShutdownEventImpl(); accessor.lifecycleService.fireWillShutdown(event); @@ -328,14 +252,13 @@ suite('BackupTracker', () => { const veto = await event.value; assert.ok(!veto); - assert.equal(accessor.workingCopyService.dirtyCount, 0); + assert.strictEqual(accessor.workingCopyService.dirtyCount, 0); - part.dispose(); - tracker.dispose(); + await cleanup(); }); test('onWillShutdown - no veto and backups cleaned up if user does not want to save (hot.exit: off)', async function () { - const [accessor, part, tracker] = await createTracker(); + const { accessor, cleanup } = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { pinned: true } }); @@ -347,7 +270,7 @@ suite('BackupTracker', () => { await model?.load(); model?.textEditorModel?.setValue('foo'); - assert.equal(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); const event = new BeforeShutdownEventImpl(); accessor.lifecycleService.fireWillShutdown(event); @@ -355,12 +278,11 @@ suite('BackupTracker', () => { assert.ok(!veto); assert.ok(accessor.backupFileService.discardedBackups.length > 0); - part.dispose(); - tracker.dispose(); + await cleanup(); }); test('onWillShutdown - save (hot.exit: off)', async function () { - const [accessor, part, tracker] = await createTracker(); + const { accessor, cleanup } = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { pinned: true } }); @@ -372,7 +294,7 @@ suite('BackupTracker', () => { await model?.load(); model?.textEditorModel?.setValue('foo'); - assert.equal(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); const event = new BeforeShutdownEventImpl(); accessor.lifecycleService.fireWillShutdown(event); @@ -380,8 +302,7 @@ suite('BackupTracker', () => { assert.ok(!veto); assert.ok(!model?.isDirty()); - part.dispose(); - tracker.dispose(); + await cleanup(); }); suite('Hot Exit', () => { @@ -488,7 +409,7 @@ suite('BackupTracker', () => { }); async function hotExitTest(this: any, setting: string, shutdownReason: ShutdownReason, multipleWindows: boolean, workspace: boolean, shouldVeto: boolean): Promise { - const [accessor, part, tracker] = await createTracker(); + const { accessor, cleanup } = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { pinned: true } }); @@ -513,18 +434,17 @@ suite('BackupTracker', () => { await model?.load(); model?.textEditorModel?.setValue('foo'); - assert.equal(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); const event = new BeforeShutdownEventImpl(); event.reason = shutdownReason; accessor.lifecycleService.fireWillShutdown(event); const veto = await event.value; - assert.equal(accessor.backupFileService.discardedBackups.length, 0); // When hot exit is set, backups should never be cleaned since the confirm result is cancel - assert.equal(veto, shouldVeto); + assert.strictEqual(accessor.backupFileService.discardedBackups.length, 0); // When hot exit is set, backups should never be cleaned since the confirm result is cancel + assert.strictEqual(veto, shouldVeto); - part.dispose(); - tracker.dispose(); + await cleanup(); } }); }); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts index ffc54667..889c25a9 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts @@ -19,6 +19,8 @@ import { BulkCellEdits, ResourceNotebookCellEdit } from 'vs/workbench/contrib/bu import { UndoRedoGroup, UndoRedoSource } from 'vs/platform/undoRedo/common/undoRedo'; import { LinkedList } from 'vs/base/common/linkedList'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; class BulkEdit { @@ -30,6 +32,7 @@ class BulkEdit { private readonly _edits: ResourceEdit[], private readonly _undoRedoGroup: UndoRedoGroup, private readonly _undoRedoSource: UndoRedoSource | undefined, + private readonly _confirmBeforeUndo: boolean, @IInstantiationService private readonly _instaService: IInstantiationService, @ILogService private readonly _logService: ILogService, ) { @@ -76,7 +79,7 @@ class BulkEdit { } const group = this._edits.slice(index, index + range); if (group[0] instanceof ResourceFileEdit) { - await this._performFileEdits(group, this._undoRedoGroup, this._undoRedoSource, progress); + await this._performFileEdits(group, this._undoRedoGroup, this._undoRedoSource, this._confirmBeforeUndo, progress); } else if (group[0] instanceof ResourceTextEdit) { await this._performTextEdits(group, this._undoRedoGroup, this._undoRedoSource, progress); } else if (group[0] instanceof ResourceNotebookCellEdit) { @@ -88,9 +91,9 @@ class BulkEdit { } } - private async _performFileEdits(edits: ResourceFileEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress) { + private async _performFileEdits(edits: ResourceFileEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, confirmBeforeUndo: boolean, progress: IProgress) { this._logService.debug('_performFileEdits', JSON.stringify(edits)); - const model = this._instaService.createInstance(BulkFileEdits, this._label || localize('workspaceEdit', "Workspace Edit"), undoRedoGroup, undoRedoSource, progress, this._token, edits); + const model = this._instaService.createInstance(BulkFileEdits, this._label || localize('workspaceEdit', "Workspace Edit"), undoRedoGroup, undoRedoSource, confirmBeforeUndo, progress, this._token, edits); await model.apply(); } @@ -118,6 +121,8 @@ export class BulkEditService implements IBulkEditService { @IInstantiationService private readonly _instaService: IInstantiationService, @ILogService private readonly _logService: ILogService, @IEditorService private readonly _editorService: IEditorService, + @ILifecycleService private readonly _lifecycleService: ILifecycleService, + @IDialogService private readonly _dialogService: IDialogService ) { } setPreviewHandler(handler: IBulkEditPreviewHandler): IDisposable { @@ -139,7 +144,7 @@ export class BulkEditService implements IBulkEditService { return { ariaSummary: localize('nothing', "Made no edits") }; } - if (this._previewHandler && !options?.suppressPreview && (options?.showPreview || edits.some(value => value.metadata?.needsConfirmation))) { + if (this._previewHandler && (options?.showPreview || edits.some(value => value.metadata?.needsConfirmation))) { edits = await this._previewHandler(edits, options); } @@ -175,18 +180,22 @@ export class BulkEditService implements IBulkEditService { undoRedoGroupRemove = this._activeUndoRedoGroups.push(undoRedoGroup); } + const label = options?.quotableLabel || options?.label; const bulkEdit = this._instaService.createInstance( BulkEdit, - options?.quotableLabel || options?.label, + label, codeEditor, options?.progress ?? Progress.None, options?.token ?? CancellationToken.None, edits, undoRedoGroup, - options?.undoRedoSource + options?.undoRedoSource, + !!options?.confirmBeforeUndo ); + let listener: IDisposable | undefined; try { + listener = this._lifecycleService.onBeforeShutdown(e => e.veto(this.shouldVeto(label), 'veto.blukEditService')); await bulkEdit.perform(); return { ariaSummary: bulkEdit.ariaMessage() }; } catch (err) { @@ -195,9 +204,20 @@ export class BulkEditService implements IBulkEditService { this._logService.error(err); throw err; } finally { + listener?.dispose(); undoRedoGroupRemove(); } } + + private async shouldVeto(label: string | undefined): Promise { + label = label || localize('fileOperation', "File operation"); + const result = await this._dialogService.confirm({ + message: localize('areYouSureQuiteBulkEdit', "Are you sure you want to quit? '{0}' is in progress.", label), + primaryButton: localize('quit', "Quit") + }); + + return !result.confirmed; + } } registerSingleton(IBulkEditService, BulkEditService, true); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts index f4ac30ca..e15f322a 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts @@ -8,20 +8,16 @@ import { WorkspaceFileEditOptions } from 'vs/editor/common/modes'; import { IFileService, FileSystemProviderCapabilities, IFileContent } from 'vs/platform/files/common/files'; import { IProgress } from 'vs/platform/progress/common/progress'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { IWorkingCopyFileService, IFileOperationUndoRedoInfo, IMoveOperation, ICopyOperation, IDeleteOperation, ICreateOperation, ICreateFileOperation } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoService, UndoRedoGroup, UndoRedoSource } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { VSBuffer } from 'vs/base/common/buffer'; import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; -import * as resources from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; - -interface IFileOperationUndoRedoInfo { - undoRedoGroupId?: number; - isUndoing?: boolean; -} +import { flatten, tail } from 'vs/base/common/arrays'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; interface IFileOperation { uris: URI[]; @@ -36,115 +32,189 @@ class Noop implements IFileOperation { } } -class RenameOperation implements IFileOperation { - +class RenameEdit { + readonly type = 'rename'; constructor( readonly newUri: URI, readonly oldUri: URI, - readonly options: WorkspaceFileEditOptions, - readonly undoRedoInfo: IFileOperationUndoRedoInfo, + readonly options: WorkspaceFileEditOptions + ) { } +} + +class RenameOperation implements IFileOperation { + + constructor( + private readonly _edits: RenameEdit[], + private readonly _undoRedoInfo: IFileOperationUndoRedoInfo, @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, @IFileService private readonly _fileService: IFileService, ) { } get uris() { - return [this.newUri, this.oldUri]; + return flatten(this._edits.map(edit => [edit.newUri, edit.oldUri])); } async perform(token: CancellationToken): Promise { - // rename - if (this.options.overwrite === undefined && this.options.ignoreIfExists && await this._fileService.exists(this.newUri)) { - return new Noop(); // not overwriting, but ignoring, and the target file exists + + const moves: IMoveOperation[] = []; + const undoes: RenameEdit[] = []; + for (const edit of this._edits) { + // check: not overwriting, but ignoring, and the target file exists + const skip = edit.options.overwrite === undefined && edit.options.ignoreIfExists && await this._fileService.exists(edit.newUri); + if (!skip) { + moves.push({ + file: { source: edit.oldUri, target: edit.newUri }, + overwrite: edit.options.overwrite + }); + + // reverse edit + undoes.push(new RenameEdit(edit.oldUri, edit.newUri, edit.options)); + } } - await this._workingCopyFileService.move([{ source: this.oldUri, target: this.newUri }], { overwrite: this.options.overwrite, ...this.undoRedoInfo }, token); - return new RenameOperation(this.oldUri, this.newUri, this.options, { isUndoing: true }, this._workingCopyFileService, this._fileService); + if (moves.length === 0) { + return new Noop(); + } + + await this._workingCopyFileService.move(moves, this._undoRedoInfo, token); + return new RenameOperation(undoes, { isUndoing: true }, this._workingCopyFileService, this._fileService); } toString(): string { - const oldBasename = resources.basename(this.oldUri); - const newBasename = resources.basename(this.newUri); - if (oldBasename !== newBasename) { - return `(rename ${oldBasename} to ${newBasename})`; - } - return `(rename ${this.oldUri} to ${this.newUri})`; + return `(rename ${this._edits.map(edit => `${edit.oldUri} to ${edit.newUri}`).join(', ')})`; } } +class CopyEdit { + readonly type = 'copy'; + constructor( + readonly newUri: URI, + readonly oldUri: URI, + readonly options: WorkspaceFileEditOptions + ) { } +} + class CopyOperation implements IFileOperation { constructor( - readonly newUri: URI, - readonly oldUri: URI, - readonly options: WorkspaceFileEditOptions, - readonly undoRedoInfo: IFileOperationUndoRedoInfo, + private readonly _edits: CopyEdit[], + private readonly _undoRedoInfo: IFileOperationUndoRedoInfo, @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, @IFileService private readonly _fileService: IFileService, @IInstantiationService private readonly _instaService: IInstantiationService ) { } get uris() { - return [this.newUri, this.oldUri]; + return flatten(this._edits.map(edit => [edit.newUri, edit.oldUri])); } async perform(token: CancellationToken): Promise { - // copy - if (this.options.overwrite === undefined && this.options.ignoreIfExists && await this._fileService.exists(this.newUri)) { - return new Noop(); // not overwriting, but ignoring, and the target file exists + + // (1) create copy operations, remove noops + const copies: ICopyOperation[] = []; + for (const edit of this._edits) { + //check: not overwriting, but ignoring, and the target file exists + const skip = edit.options.overwrite === undefined && edit.options.ignoreIfExists && await this._fileService.exists(edit.newUri); + if (!skip) { + copies.push({ file: { source: edit.oldUri, target: edit.newUri }, overwrite: edit.options.overwrite }); + } } - const stat = await this._workingCopyFileService.copy([{ source: this.oldUri, target: this.newUri }], { overwrite: this.options.overwrite, ...this.undoRedoInfo }, token); - const folder = this.options.folder || (stat.length === 1 && stat[0].isDirectory); - return this._instaService.createInstance(DeleteOperation, this.newUri, { recursive: true, folder, ...this.options }, { isUndoing: true }, false); + if (copies.length === 0) { + return new Noop(); + } + + // (2) perform the actual copy and use the return stats to build undo edits + const stats = await this._workingCopyFileService.copy(copies, this._undoRedoInfo, token); + const undoes: DeleteEdit[] = []; + + for (let i = 0; i < stats.length; i++) { + const stat = stats[i]; + const edit = this._edits[i]; + undoes.push(new DeleteEdit(stat.resource, { recursive: true, folder: this._edits[i].options.folder || stat.isDirectory, ...edit.options }, false)); + } + + return this._instaService.createInstance(DeleteOperation, undoes, { isUndoing: true }); } toString(): string { - return `(copy ${this.oldUri} to ${this.newUri})`; + return `(copy ${this._edits.map(edit => `${edit.oldUri} to ${edit.newUri}`).join(', ')})`; } } +class CreateEdit { + readonly type = 'create'; + constructor( + readonly newUri: URI, + readonly options: WorkspaceFileEditOptions, + readonly contents: VSBuffer | undefined, + ) { } +} + class CreateOperation implements IFileOperation { constructor( - readonly newUri: URI, - readonly options: WorkspaceFileEditOptions, - readonly undoRedoInfo: IFileOperationUndoRedoInfo, - readonly contents: VSBuffer | undefined, + private readonly _edits: CreateEdit[], + private readonly _undoRedoInfo: IFileOperationUndoRedoInfo, @IFileService private readonly _fileService: IFileService, @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, @IInstantiationService private readonly _instaService: IInstantiationService, + @ITextFileService private readonly _textFileService: ITextFileService ) { } get uris() { - return [this.newUri]; + return this._edits.map(edit => edit.newUri); } async perform(token: CancellationToken): Promise { - // create file - if (this.options.overwrite === undefined && this.options.ignoreIfExists && await this._fileService.exists(this.newUri)) { - return new Noop(); // not overwriting, but ignoring, and the target file exists + + const folderCreates: ICreateOperation[] = []; + const fileCreates: ICreateFileOperation[] = []; + const undoes: DeleteEdit[] = []; + + for (const edit of this._edits) { + if (edit.options.overwrite === undefined && edit.options.ignoreIfExists && await this._fileService.exists(edit.newUri)) { + continue; // not overwriting, but ignoring, and the target file exists + } + if (edit.options.folder) { + folderCreates.push({ resource: edit.newUri }); + } else { + // If the contents are part of the edit they include the encoding, thus use them. Otherwise get the encoding for a new empty file. + const encodedReadable = typeof edit.contents !== 'undefined' ? edit.contents : await this._textFileService.getEncodedReadable(edit.newUri); + fileCreates.push({ resource: edit.newUri, contents: encodedReadable, overwrite: edit.options.overwrite }); + } + undoes.push(new DeleteEdit(edit.newUri, edit.options, !edit.options.folder && !edit.contents)); } - if (this.options.folder) { - await this._workingCopyFileService.createFolder(this.newUri, { ...this.undoRedoInfo }, token); - } else { - await this._workingCopyFileService.create(this.newUri, this.contents, { overwrite: this.options.overwrite, ...this.undoRedoInfo }, token); + + if (folderCreates.length === 0 && fileCreates.length === 0) { + return new Noop(); } - return this._instaService.createInstance(DeleteOperation, this.newUri, this.options, { isUndoing: true }, !this.options.folder && !this.contents); + + await this._workingCopyFileService.createFolder(folderCreates, this._undoRedoInfo, token); + await this._workingCopyFileService.create(fileCreates, this._undoRedoInfo, token); + + return this._instaService.createInstance(DeleteOperation, undoes, { isUndoing: true }); } toString(): string { - return this.options.folder ? `create ${resources.basename(this.newUri)} folder` - : `(create ${resources.basename(this.newUri)} with ${this.contents?.byteLength || 0} bytes)`; + return `(create ${this._edits.map(edit => edit.options.folder ? `folder ${edit.newUri}` : `file ${edit.newUri} with ${edit.contents?.byteLength || 0} bytes`).join(', ')})`; } } +class DeleteEdit { + readonly type = 'delete'; + constructor( + readonly oldUri: URI, + readonly options: WorkspaceFileEditOptions, + readonly undoesCreate: boolean, + ) { } +} + class DeleteOperation implements IFileOperation { constructor( - readonly oldUri: URI, - readonly options: WorkspaceFileEditOptions, - readonly undoRedoInfo: IFileOperationUndoRedoInfo, - private readonly _undoesCreateOperation: boolean, + private _edits: DeleteEdit[], + private readonly _undoRedoInfo: IFileOperationUndoRedoInfo, @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, @IFileService private readonly _fileService: IFileService, @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -153,38 +223,58 @@ class DeleteOperation implements IFileOperation { ) { } get uris() { - return [this.oldUri]; + return this._edits.map(edit => edit.oldUri); } async perform(token: CancellationToken): Promise { // delete file - if (!await this._fileService.exists(this.oldUri)) { - if (!this.options.ignoreIfNotExists) { - throw new Error(`${this.oldUri} does not exist and can not be deleted`); - } - return new Noop(); - } - let fileContent: IFileContent | undefined; - if (!this._undoesCreateOperation && !this.options.folder) { - try { - fileContent = await this._fileService.readFile(this.oldUri); - } catch (err) { - this._logService.critical(err); + const deletes: IDeleteOperation[] = []; + const undoes: CreateEdit[] = []; + + for (const edit of this._edits) { + if (!await this._fileService.exists(edit.oldUri)) { + if (!edit.options.ignoreIfNotExists) { + throw new Error(`${edit.oldUri} does not exist and can not be deleted`); + } + continue; + } + + deletes.push({ + resource: edit.oldUri, + recursive: edit.options.recursive, + useTrash: !edit.options.skipTrashBin && this._fileService.hasCapability(edit.oldUri, FileSystemProviderCapabilities.Trash) && this._configurationService.getValue('files.enableTrash') + }); + + + // read file contents for undo operation. when a file is too large it won't be restored + let fileContent: IFileContent | undefined; + if (!edit.undoesCreate && !edit.options.folder) { + try { + fileContent = await this._fileService.readFile(edit.oldUri); + } catch (err) { + this._logService.critical(err); + } + } + if (!(typeof edit.options.maxSize === 'number' && fileContent && (fileContent?.size > edit.options.maxSize))) { + undoes.push(new CreateEdit(edit.oldUri, edit.options, fileContent?.value)); } } - const useTrash = !this.options.skipTrashBin && this._fileService.hasCapability(this.oldUri, FileSystemProviderCapabilities.Trash) && this._configurationService.getValue('files.enableTrash'); - await this._workingCopyFileService.delete([this.oldUri], { useTrash, recursive: this.options.recursive, ...this.undoRedoInfo }, token); - - if (typeof this.options.maxSize === 'number' && fileContent && (fileContent?.size > this.options.maxSize)) { + if (deletes.length === 0) { return new Noop(); } - return this._instaService.createInstance(CreateOperation, this.oldUri, this.options, { isUndoing: true }, fileContent?.value); + + await this._workingCopyFileService.delete(deletes, this._undoRedoInfo, token); + + if (undoes.length === 0) { + return new Noop(); + } + return this._instaService.createInstance(CreateOperation, undoes, { isUndoing: true }); } toString(): string { - return `(delete ${resources.basename(this.oldUri)})`; + return `(delete ${this._edits.map(edit => edit.oldUri).join(', ')})`; } } @@ -196,7 +286,8 @@ class FileUndoRedoElement implements IWorkspaceUndoRedoElement { constructor( readonly label: string, - readonly operations: IFileOperation[] + readonly operations: IFileOperation[], + readonly confirmBeforeUndo: boolean ) { this.resources = ([]).concat(...operations.map(op => op.uris)); } @@ -217,7 +308,7 @@ class FileUndoRedoElement implements IWorkspaceUndoRedoElement { } } - public toString(): string { + toString(): string { return this.operations.map(op => String(op)).join(', '); } } @@ -228,6 +319,7 @@ export class BulkFileEdits { private readonly _label: string, private readonly _undoRedoGroup: UndoRedoGroup, private readonly _undoRedoSource: UndoRedoSource | undefined, + private readonly _confirmBeforeUndo: boolean, private readonly _progress: IProgress, private readonly _token: CancellationToken, private readonly _edits: ResourceFileEdit[], @@ -238,34 +330,66 @@ export class BulkFileEdits { async apply(): Promise { const undoOperations: IFileOperation[] = []; const undoRedoInfo = { undoRedoGroupId: this._undoRedoGroup.id }; + + const edits: Array = []; for (const edit of this._edits) { + if (edit.newResource && edit.oldResource && !edit.options?.copy) { + edits.push(new RenameEdit(edit.newResource, edit.oldResource, edit.options ?? {})); + } else if (edit.newResource && edit.oldResource && edit.options?.copy) { + edits.push(new CopyEdit(edit.newResource, edit.oldResource, edit.options ?? {})); + } else if (!edit.newResource && edit.oldResource) { + edits.push(new DeleteEdit(edit.oldResource, edit.options ?? {}, false)); + } else if (edit.newResource && !edit.oldResource) { + edits.push(new CreateEdit(edit.newResource, edit.options ?? {}, undefined)); + } + } + + if (edits.length === 0) { + return; + } + + const groups: Array[] = []; + groups[0] = [edits[0]]; + + for (let i = 1; i < edits.length; i++) { + const edit = edits[i]; + const lastGroup = tail(groups); + if (lastGroup[0].type === edit.type) { + lastGroup.push(edit); + } else { + groups.push([edit]); + } + } + + for (let group of groups) { if (this._token.isCancellationRequested) { break; } - const options = edit.options || {}; let op: IFileOperation | undefined; - if (edit.newResource && edit.oldResource && !options.copy) { - // rename - op = this._instaService.createInstance(RenameOperation, edit.newResource, edit.oldResource, options, undoRedoInfo); - } else if (edit.newResource && edit.oldResource && options.copy) { - op = this._instaService.createInstance(CopyOperation, edit.newResource, edit.oldResource, options, undoRedoInfo); - } else if (!edit.newResource && edit.oldResource) { - // delete file - op = this._instaService.createInstance(DeleteOperation, edit.oldResource, options, undoRedoInfo, false); - } else if (edit.newResource && !edit.oldResource) { - // create file - op = this._instaService.createInstance(CreateOperation, edit.newResource, options, undoRedoInfo, undefined); + switch (group[0].type) { + case 'rename': + op = this._instaService.createInstance(RenameOperation, group, undoRedoInfo); + break; + case 'copy': + op = this._instaService.createInstance(CopyOperation, group, undoRedoInfo); + break; + case 'delete': + op = this._instaService.createInstance(DeleteOperation, group, undoRedoInfo); + break; + case 'create': + op = this._instaService.createInstance(CreateOperation, group, undoRedoInfo); + break; } + if (op) { const undoOp = await op.perform(this._token); undoOperations.push(undoOp); } - this._progress.report(undefined); } - this._undoRedoService.pushElement(new FileUndoRedoElement(this._label, undoOperations), this._undoRedoGroup, this._undoRedoSource); + this._undoRedoService.pushElement(new FileUndoRedoElement(this._label, undoOperations, this._confirmBeforeUndo), this._undoRedoGroup, this._undoRedoSource); } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index ab0172ee..3b35a67b 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -17,7 +17,7 @@ import { BulkEditPreviewProvider, BulkFileOperations, BulkFileOperationType } fr 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 { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts index e9dddb2a..7d1348b2 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts @@ -409,14 +409,14 @@ export class CategoryElementRenderer implements ITreeRenderer('accessibilityHelpWidgetVisible', false); -class AccessibilityHelpController extends Disposable implements IEditorContribution { +export class AccessibilityHelpController extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.accessibilityHelpController'; diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts index c3ede411..0f7a8480 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts @@ -8,10 +8,10 @@ import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { registerDiffEditorContribution } from 'vs/editor/browser/editorExtensions'; import { IDiffEditorContribution } from 'vs/editor/common/editorCommon'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; +import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; import { IDiffComputationResult } from 'vs/editor/common/services/editorWorkerService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; const enum WidgetState { @@ -48,7 +48,7 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont Severity.Warning, nls.localize('hintTimeout', "The diff algorithm was stopped early (after {0} ms.)", this._diffEditor.maxComputationTime), [{ - label: nls.localize('removeTimeout', "Remove limit"), + label: nls.localize('removeTimeout', "Remove Limit"), run: () => { this._configurationService.updateValue('diffEditor.maxComputationTime', 0); } @@ -94,7 +94,7 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont private _onDidClickHelperWidget(): void { if (this._state === WidgetState.HintWhitespace) { - this._configurationService.updateValue('diffEditor.ignoreTrimWhitespace', false, ConfigurationTarget.USER); + this._configurationService.updateValue('diffEditor.ignoreTrimWhitespace', false); } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts b/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts index 69ee0950..657af6f1 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts @@ -9,7 +9,7 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as types from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { LanguageIdentifier } from 'vs/editor/common/modes'; -import { CharacterPair, CommentRule, FoldingRules, IAutoClosingPair, IAutoClosingPairConditional, IndentationRule, LanguageConfiguration } from 'vs/editor/common/modes/languageConfiguration'; +import { CharacterPair, CommentRule, EnterAction, FoldingRules, IAutoClosingPair, IAutoClosingPairConditional, IndentAction, IndentationRule, LanguageConfiguration, OnEnterRule } from 'vs/editor/common/modes/languageConfiguration'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { IModeService } from 'vs/editor/common/services/modeService'; import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; @@ -31,6 +31,19 @@ interface IIndentationRules { unIndentedLinePattern?: string | IRegExp; } +interface IEnterAction { + indent: 'none' | 'indent' | 'indentOutdent' | 'outdent'; + appendText?: string; + removeText?: number; +} + +interface IOnEnterRule { + beforeText: string | IRegExp; + afterText?: string | IRegExp; + previousLineText?: string | IRegExp; + action: IEnterAction; +} + interface ILanguageConfiguration { comments?: CommentRule; brackets?: CharacterPair[]; @@ -40,6 +53,7 @@ interface ILanguageConfiguration { indentationRules?: IIndentationRules; folding?: FoldingRules; autoCloseBefore?: string; + onEnterRules?: IOnEnterRule[]; } function isStringArr(something: string[] | null): something is string[] { @@ -93,7 +107,7 @@ export class LanguageConfigurationFileHandler { } this._done[languageIdentifier.id] = true; - let configurationFiles = this._modeService.getConfigurationFiles(languageIdentifier.language); + const configurationFiles = this._modeService.getConfigurationFiles(languageIdentifier.language); configurationFiles.forEach((configFileLocation) => this._handleConfigFile(languageIdentifier, configFileLocation)); } @@ -254,18 +268,82 @@ export class LanguageConfigurationFileHandler { return result; } - // private _mapCharacterPairs(pairs: Array): IAutoClosingPairConditional[] { - // return pairs.map(pair => { - // if (Array.isArray(pair)) { - // return { open: pair[0], close: pair[1] }; - // } - // return pair; - // }); - // } + private _extractValidOnEnterRules(languageIdentifier: LanguageIdentifier, configuration: ILanguageConfiguration): OnEnterRule[] | null { + const source = configuration.onEnterRules; + if (typeof source === 'undefined') { + return null; + } + if (!Array.isArray(source)) { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules\` to be an array.`); + return null; + } + + let result: OnEnterRule[] | null = null; + for (let i = 0, len = source.length; i < len; i++) { + const onEnterRule = source[i]; + if (!types.isObject(onEnterRule)) { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}]\` to be an object.`); + continue; + } + if (!types.isObject(onEnterRule.action)) { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}].action\` to be an object.`); + continue; + } + let indentAction: IndentAction; + if (onEnterRule.action.indent === 'none') { + indentAction = IndentAction.None; + } else if (onEnterRule.action.indent === 'indent') { + indentAction = IndentAction.Indent; + } else if (onEnterRule.action.indent === 'indentOutdent') { + indentAction = IndentAction.IndentOutdent; + } else if (onEnterRule.action.indent === 'outdent') { + indentAction = IndentAction.Outdent; + } else { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}].action.indent\` to be 'none', 'indent', 'indentOutdent' or 'outdent'.`); + continue; + } + const action: EnterAction = { indentAction }; + if (onEnterRule.action.appendText) { + if (typeof onEnterRule.action.appendText === 'string') { + action.appendText = onEnterRule.action.appendText; + } else { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}].action.appendText\` to be undefined or a string.`); + } + } + if (onEnterRule.action.removeText) { + if (typeof onEnterRule.action.removeText === 'number') { + action.removeText = onEnterRule.action.removeText; + } else { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}].action.removeText\` to be undefined or a number.`); + } + } + const beforeText = this._parseRegex(languageIdentifier, `onEnterRules[${i}].beforeText`, onEnterRule.beforeText); + if (!beforeText) { + continue; + } + const resultingOnEnterRule: OnEnterRule = { beforeText, action }; + if (onEnterRule.afterText) { + const afterText = this._parseRegex(languageIdentifier, `onEnterRules[${i}].afterText`, onEnterRule.afterText); + if (afterText) { + resultingOnEnterRule.afterText = afterText; + } + } + if (onEnterRule.previousLineText) { + const previousLineText = this._parseRegex(languageIdentifier, `onEnterRules[${i}].previousLineText`, onEnterRule.previousLineText); + if (previousLineText) { + resultingOnEnterRule.previousLineText = previousLineText; + } + } + result = result || []; + result.push(resultingOnEnterRule); + } + + return result; + } private _handleConfig(languageIdentifier: LanguageIdentifier, configuration: ILanguageConfiguration): void { - let richEditConfig: LanguageConfiguration = {}; + const richEditConfig: LanguageConfiguration = {}; const comments = this._extractValidCommentRule(languageIdentifier, configuration); if (comments) { @@ -293,25 +371,21 @@ export class LanguageConfigurationFileHandler { } if (configuration.wordPattern) { - try { - let wordPattern = this._parseRegex(configuration.wordPattern); - if (wordPattern) { - richEditConfig.wordPattern = wordPattern; - } - } catch (error) { - // Malformed regexes are ignored + const wordPattern = this._parseRegex(languageIdentifier, `wordPattern`, configuration.wordPattern); + if (wordPattern) { + richEditConfig.wordPattern = wordPattern; } } if (configuration.indentationRules) { - let indentationRules = this._mapIndentationRules(configuration.indentationRules); + const indentationRules = this._mapIndentationRules(languageIdentifier, configuration.indentationRules); if (indentationRules) { richEditConfig.indentationRules = indentationRules; } } if (configuration.folding) { - let markers = configuration.folding.markers; + const markers = configuration.folding.markers; richEditConfig.folding = { offSide: configuration.folding.offSide, @@ -319,44 +393,66 @@ export class LanguageConfigurationFileHandler { }; } + const onEnterRules = this._extractValidOnEnterRules(languageIdentifier, configuration); + if (onEnterRules) { + richEditConfig.onEnterRules = onEnterRules; + } + LanguageConfigurationRegistry.register(languageIdentifier, richEditConfig); } - private _parseRegex(value: string | IRegExp) { + private _parseRegex(languageIdentifier: LanguageIdentifier, confPath: string, value: string | IRegExp) { if (typeof value === 'string') { - return new RegExp(value, ''); - } else if (typeof value === 'object') { - return new RegExp(value.pattern, value.flags); + try { + return new RegExp(value, ''); + } catch (err) { + console.warn(`[${languageIdentifier.language}]: Invalid regular expression in \`${confPath}\`: `, err); + return null; + } } - + if (types.isObject(value)) { + if (typeof value.pattern !== 'string') { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`${confPath}.pattern\` to be a string.`); + return null; + } + if (typeof value.flags !== 'undefined' && typeof value.flags !== 'string') { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`${confPath}.flags\` to be a string.`); + return null; + } + try { + return new RegExp(value.pattern, value.flags); + } catch (err) { + console.warn(`[${languageIdentifier.language}]: Invalid regular expression in \`${confPath}\`: `, err); + return null; + } + } + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`${confPath}\` to be a string or an object.`); return null; } - private _mapIndentationRules(indentationRules: IIndentationRules): IndentationRule | null { - try { - let increaseIndentPattern = this._parseRegex(indentationRules.increaseIndentPattern); - let decreaseIndentPattern = this._parseRegex(indentationRules.decreaseIndentPattern); - - if (increaseIndentPattern && decreaseIndentPattern) { - let result: IndentationRule = { - increaseIndentPattern: increaseIndentPattern, - decreaseIndentPattern: decreaseIndentPattern - }; - - if (indentationRules.indentNextLinePattern) { - result.indentNextLinePattern = this._parseRegex(indentationRules.indentNextLinePattern); - } - if (indentationRules.unIndentedLinePattern) { - result.unIndentedLinePattern = this._parseRegex(indentationRules.unIndentedLinePattern); - } - - return result; - } - } catch (error) { - // Malformed regexes are ignored + private _mapIndentationRules(languageIdentifier: LanguageIdentifier, indentationRules: IIndentationRules): IndentationRule | null { + const increaseIndentPattern = this._parseRegex(languageIdentifier, `indentationRules.increaseIndentPattern`, indentationRules.increaseIndentPattern); + if (!increaseIndentPattern) { + return null; + } + const decreaseIndentPattern = this._parseRegex(languageIdentifier, `indentationRules.decreaseIndentPattern`, indentationRules.decreaseIndentPattern); + if (!decreaseIndentPattern) { + return null; } - return null; + const result: IndentationRule = { + increaseIndentPattern: increaseIndentPattern, + decreaseIndentPattern: decreaseIndentPattern + }; + + if (indentationRules.indentNextLinePattern) { + result.indentNextLinePattern = this._parseRegex(languageIdentifier, `indentationRules.indentNextLinePattern`, indentationRules.indentNextLinePattern); + } + if (indentationRules.unIndentedLinePattern) { + result.unIndentedLinePattern = this._parseRegex(languageIdentifier, `indentationRules.unIndentedLinePattern`, indentationRules.unIndentedLinePattern); + } + + return result; } } @@ -601,6 +697,101 @@ const schema: IJSONSchema = { } } } + }, + onEnterRules: { + type: 'array', + description: nls.localize('schema.onEnterRules', 'The language\'s rules to be evaluated when pressing Enter.'), + items: { + type: 'object', + description: nls.localize('schema.onEnterRules', 'The language\'s rules to be evaluated when pressing Enter.'), + required: ['beforeText', 'action'], + properties: { + beforeText: { + type: ['string', 'object'], + description: nls.localize('schema.onEnterRules.beforeText', 'This rule will only execute if the text before the cursor matches this regular expression.'), + properties: { + pattern: { + type: 'string', + description: nls.localize('schema.onEnterRules.beforeText.pattern', 'The RegExp pattern for beforeText.'), + default: '', + }, + flags: { + type: 'string', + description: nls.localize('schema.onEnterRules.beforeText.flags', 'The RegExp flags for beforeText.'), + default: '', + pattern: '^([gimuy]+)$', + patternErrorMessage: nls.localize('schema.onEnterRules.beforeText.errorMessage', 'Must match the pattern `/^([gimuy]+)$/`.') + } + } + }, + afterText: { + type: ['string', 'object'], + description: nls.localize('schema.onEnterRules.afterText', 'This rule will only execute if the text after the cursor matches this regular expression.'), + properties: { + pattern: { + type: 'string', + description: nls.localize('schema.onEnterRules.afterText.pattern', 'The RegExp pattern for afterText.'), + default: '', + }, + flags: { + type: 'string', + description: nls.localize('schema.onEnterRules.afterText.flags', 'The RegExp flags for afterText.'), + default: '', + pattern: '^([gimuy]+)$', + patternErrorMessage: nls.localize('schema.onEnterRules.afterText.errorMessage', 'Must match the pattern `/^([gimuy]+)$/`.') + } + } + }, + previousLineText: { + type: ['string', 'object'], + description: nls.localize('schema.onEnterRules.previousLineText', 'This rule will only execute if the text above the line matches this regular expression.'), + properties: { + pattern: { + type: 'string', + description: nls.localize('schema.onEnterRules.previousLineText.pattern', 'The RegExp pattern for previousLineText.'), + default: '', + }, + flags: { + type: 'string', + description: nls.localize('schema.onEnterRules.previousLineText.flags', 'The RegExp flags for previousLineText.'), + default: '', + pattern: '^([gimuy]+)$', + patternErrorMessage: nls.localize('schema.onEnterRules.previousLineText.errorMessage', 'Must match the pattern `/^([gimuy]+)$/`.') + } + } + }, + action: { + type: ['string', 'object'], + description: nls.localize('schema.onEnterRules.action', 'The action to execute.'), + required: ['indent'], + default: { 'indent': 'indent' }, + properties: { + indent: { + type: 'string', + description: nls.localize('schema.onEnterRules.action.indent', "Describe what to do with the indentation"), + default: 'indent', + enum: ['none', 'indent', 'indentOutdent', 'outdent'], + markdownEnumDescriptions: [ + nls.localize('schema.onEnterRules.action.indent.none', "Insert new line and copy the previous line's indentation."), + nls.localize('schema.onEnterRules.action.indent.indent', "Insert new line and indent once (relative to the previous line's indentation)."), + nls.localize('schema.onEnterRules.action.indent.indentOutdent', "Insert two new lines:\n - the first one indented which will hold the cursor\n - the second one at the same indentation level"), + nls.localize('schema.onEnterRules.action.indent.outdent', "Insert new line and outdent once (relative to the previous line's indentation).") + ] + }, + appendText: { + type: 'string', + description: nls.localize('schema.onEnterRules.action.appendText', 'Describes text to be appended after the new line and after the indentation.'), + default: '', + }, + removeText: { + type: 'number', + description: nls.localize('schema.onEnterRules.action.removeText', 'Describes the number of characters to remove from the new line\'s indentation.'), + default: 0, + } + } + } + } + } } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/largeFileOptimizations.ts b/src/vs/workbench/contrib/codeEditor/browser/largeFileOptimizations.ts index f7a7ea98..0d817416 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/largeFileOptimizations.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/largeFileOptimizations.ts @@ -46,7 +46,7 @@ export class LargeFileOptimizationsWarner extends Disposable implements IEditorC this._notificationService.prompt(Severity.Info, message, [ { - label: nls.localize('removeOptimizations', "Forcefully enable features"), + label: nls.localize('removeOptimizations', "Forcefully Enable Features"), run: () => { this._configurationService.updateValue(`editor.largeFileOptimizations`, false).then(() => { this._notificationService.info(nls.localize('reopenFilePrompt', "Please reopen file in order for this setting to take effect.")); diff --git a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts new file mode 100644 index 00000000..bdb42efa --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts @@ -0,0 +1,429 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IBreadcrumbsDataSource, IOutline, IOutlineCreator, IOutlineListConfig, IOutlineService, OutlineChangeEvent, OutlineConfigKeys, OutlineTarget, } from 'vs/workbench/services/outline/browser/outline'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IEditorPane } from 'vs/workbench/common/editor'; +import { DocumentSymbolComparator, DocumentSymbolAccessibilityProvider, DocumentSymbolRenderer, DocumentSymbolFilter, DocumentSymbolGroupRenderer, DocumentSymbolIdentityProvider, DocumentSymbolNavigationLabelProvider, DocumentSymbolVirtualDelegate } from 'vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree'; +import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { OutlineGroup, OutlineElement, OutlineModel, TreeElement, IOutlineMarker } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { DocumentSymbolProviderRegistry } from 'vs/editor/common/modes'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { raceCancellation, TimeoutTimer, timeout, Barrier } from 'vs/base/common/async'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { URI } from 'vs/base/common/uri'; +import { ITextModel } from 'vs/editor/common/model'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IPosition } from 'vs/editor/common/core/position'; +import { ScrollType } from 'vs/editor/common/editorCommon'; +import { Range } from 'vs/editor/common/core/range'; +import { IEditorOptions, TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; +import { IDataSource } from 'vs/base/browser/ui/tree/tree'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { localize } from 'vs/nls'; +import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDecorationService'; +import { MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { isEqual } from 'vs/base/common/resources'; + +type DocumentSymbolItem = OutlineGroup | OutlineElement; + +class DocumentSymbolBreadcrumbsSource implements IBreadcrumbsDataSource{ + + private _breadcrumbs: (OutlineGroup | OutlineElement)[] = []; + + constructor( + private readonly _editor: ICodeEditor, + @ITextResourceConfigurationService private readonly _textResourceConfigurationService: ITextResourceConfigurationService, + ) { } + + getBreadcrumbElements(): readonly DocumentSymbolItem[] { + return this._breadcrumbs; + } + + clear(): void { + this._breadcrumbs = []; + } + + update(model: OutlineModel, position: IPosition): void { + const newElements = this._computeBreadcrumbs(model, position); + this._breadcrumbs = newElements; + } + + private _computeBreadcrumbs(model: OutlineModel, position: IPosition): Array { + let item: OutlineGroup | OutlineElement | undefined = model.getItemEnclosingPosition(position); + if (!item) { + return []; + } + let chain: Array = []; + while (item) { + chain.push(item); + let parent: any = item.parent; + if (parent instanceof OutlineModel) { + break; + } + if (parent instanceof OutlineGroup && parent.parent && parent.parent.children.size === 1) { + break; + } + item = parent; + } + let result: Array = []; + for (let i = chain.length - 1; i >= 0; i--) { + let element = chain[i]; + if (this._isFiltered(element)) { + break; + } + result.push(element); + } + if (result.length === 0) { + return []; + } + return result; + } + + private _isFiltered(element: TreeElement): boolean { + if (!(element instanceof OutlineElement)) { + return false; + } + const key = `breadcrumbs.${DocumentSymbolFilter.kindToConfigName[element.symbol.kind]}`; + let uri: URI | undefined; + if (this._editor && this._editor.getModel()) { + const model = this._editor.getModel() as ITextModel; + uri = model.uri; + } + return !this._textResourceConfigurationService.getValue(uri, key); + } +} + +class DocumentSymbolsOutline implements IOutline { + + private readonly _disposables = new DisposableStore(); + private readonly _onDidChange = new Emitter(); + + readonly onDidChange: Event = this._onDidChange.event; + + private _outlineModel?: OutlineModel; + private _outlineDisposables = new DisposableStore(); + + private readonly _breadcrumbsDataSource: DocumentSymbolBreadcrumbsSource; + + readonly config: IOutlineListConfig; + + readonly outlineKind = 'documentSymbols'; + + get activeElement(): DocumentSymbolItem | undefined { + const posistion = this._editor.getPosition(); + if (!posistion || !this._outlineModel) { + return undefined; + } else { + return this._outlineModel.getItemEnclosingPosition(posistion); + } + } + + constructor( + private readonly _editor: ICodeEditor, + target: OutlineTarget, + firstLoadBarrier: Barrier, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService, + @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + + this._breadcrumbsDataSource = new DocumentSymbolBreadcrumbsSource(_editor, textResourceConfigurationService); + const delegate = new DocumentSymbolVirtualDelegate(); + const renderers = [new DocumentSymbolGroupRenderer(), instantiationService.createInstance(DocumentSymbolRenderer, true)]; + const treeDataSource: IDataSource = { + getChildren: (parent) => { + if (parent instanceof OutlineElement || parent instanceof OutlineGroup) { + return parent.children.values(); + } + if (parent === this && this._outlineModel) { + return this._outlineModel.children.values(); + } + return []; + } + }; + const comparator = new DocumentSymbolComparator(); + const options = { + collapseByDefault: target === OutlineTarget.Breadcrumbs, + expandOnlyOnTwistieClick: true, + multipleSelectionSupport: false, + identityProvider: new DocumentSymbolIdentityProvider(), + keyboardNavigationLabelProvider: new DocumentSymbolNavigationLabelProvider(), + accessibilityProvider: new DocumentSymbolAccessibilityProvider(localize('document', "Document Symbols")), + filter: target === OutlineTarget.OutlinePane + ? instantiationService.createInstance(DocumentSymbolFilter, 'outline') + : target === OutlineTarget.Breadcrumbs + ? instantiationService.createInstance(DocumentSymbolFilter, 'breadcrumbs') + : undefined + }; + + this.config = { + breadcrumbsDataSource: this._breadcrumbsDataSource, + delegate, + renderers, + treeDataSource, + comparator, + options, + quickPickDataSource: { getQuickPickElements: () => { throw new Error('not implemented'); } } + }; + + + // update as language, model, providers changes + this._disposables.add(DocumentSymbolProviderRegistry.onDidChange(_ => this._createOutline())); + this._disposables.add(this._editor.onDidChangeModel(_ => this._createOutline())); + this._disposables.add(this._editor.onDidChangeModelLanguage(_ => this._createOutline())); + + // update soon'ish as model content change + const updateSoon = new TimeoutTimer(); + this._disposables.add(updateSoon); + this._disposables.add(this._editor.onDidChangeModelContent(event => { + const timeout = OutlineModel.getRequestDelay(this._editor!.getModel()); + updateSoon.cancelAndSet(() => this._createOutline(event), timeout); + })); + + // stop when editor dies + this._disposables.add(this._editor.onDidDispose(() => this._outlineDisposables.clear())); + + // initial load + this._createOutline().finally(() => firstLoadBarrier.open()); + } + + dispose(): void { + this._disposables.dispose(); + this._outlineDisposables.dispose(); + } + + get isEmpty(): boolean { + return !this._outlineModel || TreeElement.empty(this._outlineModel); + } + + async reveal(entry: DocumentSymbolItem, options: IEditorOptions, sideBySide: boolean): Promise { + const model = OutlineModel.get(entry); + if (!model || !(entry instanceof OutlineElement)) { + return; + } + await this._codeEditorService.openCodeEditor({ + resource: model.uri, + options: { + ...options, + selection: Range.collapseToStart(entry.symbol.selectionRange), + selectionRevealType: TextEditorSelectionRevealType.NearTopIfOutsideViewport, + } + }, this._editor, sideBySide); + } + + preview(entry: DocumentSymbolItem): IDisposable { + if (!(entry instanceof OutlineElement)) { + return Disposable.None; + } + + const { symbol } = entry; + this._editor.revealRangeInCenterIfOutsideViewport(symbol.range, ScrollType.Smooth); + const ids = this._editor.deltaDecorations([], [{ + range: symbol.range, + options: { + className: 'rangeHighlight', + isWholeLine: true + } + }]); + return toDisposable(() => this._editor.deltaDecorations(ids, [])); + } + + captureViewState(): IDisposable { + const viewState = this._editor.saveViewState(); + return toDisposable(() => { + if (viewState) { + this._editor.restoreViewState(viewState); + } + }); + } + + private async _createOutline(contentChangeEvent?: IModelContentChangedEvent): Promise { + + this._outlineDisposables.clear(); + if (!contentChangeEvent) { + this._setOutlineModel(undefined); + } + + if (!this._editor.hasModel()) { + return; + } + const buffer = this._editor.getModel(); + if (!DocumentSymbolProviderRegistry.has(buffer)) { + return; + } + + const cts = new CancellationTokenSource(); + const versionIdThen = buffer.getVersionId(); + const timeoutTimer = new TimeoutTimer(); + + this._outlineDisposables.add(timeoutTimer); + this._outlineDisposables.add(toDisposable(() => cts.dispose(true))); + + try { + let model = await OutlineModel.create(buffer, cts.token); + if (cts.token.isCancellationRequested) { + // cancelled -> do nothing + return; + } + + if (TreeElement.empty(model) || !this._editor.hasModel()) { + // empty -> no outline elements + this._setOutlineModel(model); + return; + } + + // heuristic: when the symbols-to-lines ratio changes by 50% between edits + // wait a little (and hope that the next change isn't as drastic). + if (contentChangeEvent && this._outlineModel && buffer.getLineCount() >= 25) { + const newSize = TreeElement.size(model); + const newLength = buffer.getValueLength(); + const newRatio = newSize / newLength; + const oldSize = TreeElement.size(this._outlineModel); + const oldLength = newLength - contentChangeEvent.changes.reduce((prev, value) => prev + value.rangeLength, 0); + const oldRatio = oldSize / oldLength; + if (newRatio <= oldRatio * 0.5 || newRatio >= oldRatio * 1.5) { + // wait for a better state and ignore current model when more + // typing has happened + const value = await raceCancellation(timeout(2000).then(() => true), cts.token, false); + if (!value) { + return; + } + } + } + + // copy the model + model = model.adopt(); + + // feature: show markers with outline element + this._applyMarkersToOutline(model); + this._outlineDisposables.add(this._markerDecorationsService.onDidChangeMarker(textModel => { + if (isEqual(model.uri, textModel.uri)) { + this._applyMarkersToOutline(model); + this._onDidChange.fire({}); + } + })); + this._outlineDisposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(OutlineConfigKeys.problemsEnabled)) { + if (this._configurationService.getValue(OutlineConfigKeys.problemsEnabled)) { + this._applyMarkersToOutline(model); + } else { + model.updateMarker([]); + } + this._onDidChange.fire({}); + } + if (e.affectsConfiguration('outline')) { + // outline filtering, problems on/off + this._onDidChange.fire({}); + } + if (e.affectsConfiguration('breadcrumbs') && this._editor.hasModel()) { + // breadcrumbs filtering + this._breadcrumbsDataSource.update(model, this._editor.getPosition()); + this._onDidChange.fire({}); + } + })); + + // feature: toggle icons + this._outlineDisposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(OutlineConfigKeys.icons)) { + this._onDidChange.fire({}); + } + if (e.affectsConfiguration('outline')) { + this._onDidChange.fire({}); + } + })); + + // feature: update active when cursor changes + this._outlineDisposables.add(this._editor.onDidChangeCursorPosition(_ => { + timeoutTimer.cancelAndSet(() => { + if (!buffer.isDisposed() && versionIdThen === buffer.getVersionId() && this._editor.hasModel()) { + this._breadcrumbsDataSource.update(model, this._editor.getPosition()); + this._onDidChange.fire({ affectOnlyActiveElement: true }); + } + }, 150); + })); + + // update properties, send event + this._setOutlineModel(model); + + } catch (err) { + this._setOutlineModel(undefined); + onUnexpectedError(err); + } + } + + private _applyMarkersToOutline(model: OutlineModel | undefined): void { + if (!model || !this._configurationService.getValue(OutlineConfigKeys.problemsEnabled)) { + return; + } + const markers: IOutlineMarker[] = []; + for (const [range, marker] of this._markerDecorationsService.getLiveMarkers(model.uri)) { + if (marker.severity === MarkerSeverity.Error || marker.severity === MarkerSeverity.Warning) { + markers.push({ ...range, severity: marker.severity }); + } + } + model.updateMarker(markers); + } + + private _setOutlineModel(model: OutlineModel | undefined) { + const position = this._editor.getPosition(); + if (!position || !model) { + this._outlineModel = undefined; + this._breadcrumbsDataSource.clear(); + } else { + if (!this._outlineModel?.merge(model)) { + this._outlineModel = model; + } + this._breadcrumbsDataSource.update(model, position); + } + this._onDidChange.fire({}); + } +} + +class DocumentSymbolsOutlineCreator implements IOutlineCreator { + + readonly dispose: () => void; + + constructor( + @IOutlineService outlineService: IOutlineService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + const reg = outlineService.registerOutlineCreator(this); + this.dispose = () => reg.dispose(); + } + + matches(candidate: IEditorPane): candidate is IEditorPane { + const ctrl = candidate.getControl(); + return isCodeEditor(ctrl) || isDiffEditor(ctrl); + } + + async createOutline(pane: IEditorPane, target: OutlineTarget, _token: CancellationToken): Promise | undefined> { + const control = pane.getControl(); + let editor: ICodeEditor | undefined; + if (isCodeEditor(control)) { + editor = control; + } else if (isDiffEditor(control)) { + editor = control.getModifiedEditor(); + } + if (!editor) { + return undefined; + } + const firstLoadBarrier = new Barrier(); + const result = this._instantiationService.createInstance(DocumentSymbolsOutline, editor, target, firstLoadBarrier); + await firstLoadBarrier.wait(); + return result; + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DocumentSymbolsOutlineCreator, LifecyclePhase.Eventually); diff --git a/src/vs/editor/contrib/documentSymbols/media/outlineTree.css b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.css similarity index 77% rename from src/vs/editor/contrib/documentSymbols/media/outlineTree.css rename to src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.css index ccdfe57a..107c2999 100644 --- a/src/vs/editor/contrib/documentSymbols/media/outlineTree.css +++ b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.css @@ -20,11 +20,7 @@ color: var(--outline-element-color); } -.monaco-list .outline-element .monaco-icon-label-container .monaco-highlighted-label, -.monaco-list .outline-element .monaco-icon-label-container .label-description { - white-space: nowrap; -} - +.monaco-breadcrumbs .outline-element .outline-element-decoration, .monaco-list .outline-element .outline-element-decoration { opacity: 0.75; font-size: 90%; @@ -35,10 +31,17 @@ color: var(--outline-element-color); } +/* when showing in breadcrumbs than hide a few things, like markers or descriptions */ +.monaco-breadcrumbs .outline-element .monaco-icon-label-container .monaco-icon-description-container, +.monaco-breadcrumbs .outline-element .outline-element-decoration { + display: none; +} + .monaco-list .outline-element .outline-element-decoration.bubble { font-family: codicon; font-size: 14px; opacity: 0.4; + padding-right: 8px; } .monaco-list .outline-element .outline-element-icon { diff --git a/src/vs/editor/contrib/documentSymbols/outlineTree.ts b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts similarity index 82% rename from src/vs/editor/contrib/documentSymbols/outlineTree.ts rename to src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts index c0ef5526..64b463a0 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineTree.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts @@ -3,35 +3,32 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./documentSymbolsTree'; import * as dom from 'vs/base/browser/dom'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IDataSource, ITreeNode, ITreeRenderer, ITreeSorter, ITreeFilter } from 'vs/base/browser/ui/tree/tree'; +import { ITreeNode, ITreeRenderer, ITreeFilter } from 'vs/base/browser/ui/tree/tree'; import { createMatches, FuzzyScore } from 'vs/base/common/filters'; -import 'vs/css!./media/outlineTree'; -import 'vs/css!./media/symbol-icons'; import { Range } from 'vs/editor/common/core/range'; import { SymbolKind, SymbolKinds, SymbolTag } from 'vs/editor/common/modes'; import { OutlineElement, OutlineGroup, OutlineModel } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { localize } from 'vs/nls'; -import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { OutlineConfigKeys } from 'vs/editor/contrib/documentSymbols/outline'; import { MarkerSeverity } from 'vs/platform/markers/common/markers'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { registerColor, listErrorForeground, listWarningForeground, foreground } from 'vs/platform/theme/common/colorRegistry'; import { IdleValue } from 'vs/base/common/async'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; -import { URI } from 'vs/base/common/uri'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { Iterable } from 'vs/base/common/iterator'; import { Codicon } from 'vs/base/common/codicons'; +import { IOutlineComparator, OutlineConfigKeys } from 'vs/workbench/services/outline/browser/outline'; -export type OutlineItem = OutlineGroup | OutlineElement; +export type DocumentSymbolItem = OutlineGroup | OutlineElement; -export class OutlineNavigationLabelProvider implements IKeyboardNavigationLabelProvider { +export class DocumentSymbolNavigationLabelProvider implements IKeyboardNavigationLabelProvider { - getKeyboardNavigationLabel(element: OutlineItem): { toString(): string; } { + getKeyboardNavigationLabel(element: DocumentSymbolItem): { toString(): string; } { if (element instanceof OutlineGroup) { return element.label; } else { @@ -40,15 +37,14 @@ export class OutlineNavigationLabelProvider implements IKeyboardNavigationLabelP } } -export class OutlineAccessibilityProvider implements IListAccessibilityProvider { +export class DocumentSymbolAccessibilityProvider implements IListAccessibilityProvider { - constructor(private readonly ariaLabel: string) { } + constructor(private readonly _ariaLabel: string) { } getWidgetAriaLabel(): string { - return this.ariaLabel; + return this._ariaLabel; } - - getAriaLabel(element: OutlineItem): string | null { + getAriaLabel(element: DocumentSymbolItem): string | null { if (element instanceof OutlineGroup) { return element.label; } else { @@ -57,22 +53,22 @@ export class OutlineAccessibilityProvider implements IListAccessibilityProvider< } } -export class OutlineIdentityProvider implements IIdentityProvider { - getId(element: OutlineItem): { toString(): string; } { +export class DocumentSymbolIdentityProvider implements IIdentityProvider { + getId(element: DocumentSymbolItem): { toString(): string; } { return element.id; } } -export class OutlineGroupTemplate { - static readonly id = 'OutlineGroupTemplate'; +class DocumentSymbolGroupTemplate { + static readonly id = 'DocumentSymbolGroupTemplate'; constructor( readonly labelContainer: HTMLElement, readonly label: HighlightedLabel, ) { } } -export class OutlineElementTemplate { - static readonly id = 'OutlineElementTemplate'; +class DocumentSymbolTemplate { + static readonly id = 'DocumentSymbolTemplate'; constructor( readonly container: HTMLElement, readonly iconLabel: IconLabel, @@ -81,70 +77,66 @@ export class OutlineElementTemplate { ) { } } -export class OutlineVirtualDelegate implements IListVirtualDelegate { +export class DocumentSymbolVirtualDelegate implements IListVirtualDelegate { - getHeight(_element: OutlineItem): number { + getHeight(_element: DocumentSymbolItem): number { return 22; } - getTemplateId(element: OutlineItem): string { - if (element instanceof OutlineGroup) { - return OutlineGroupTemplate.id; - } else { - return OutlineElementTemplate.id; - } + getTemplateId(element: DocumentSymbolItem): string { + return element instanceof OutlineGroup + ? DocumentSymbolGroupTemplate.id + : DocumentSymbolTemplate.id; } } -export class OutlineGroupRenderer implements ITreeRenderer { +export class DocumentSymbolGroupRenderer implements ITreeRenderer { - readonly templateId: string = OutlineGroupTemplate.id; + readonly templateId: string = DocumentSymbolGroupTemplate.id; - renderTemplate(container: HTMLElement): OutlineGroupTemplate { + renderTemplate(container: HTMLElement): DocumentSymbolGroupTemplate { const labelContainer = dom.$('.outline-element-label'); container.classList.add('outline-element'); dom.append(container, labelContainer); - return new OutlineGroupTemplate(labelContainer, new HighlightedLabel(labelContainer, true)); + return new DocumentSymbolGroupTemplate(labelContainer, new HighlightedLabel(labelContainer, true)); } - renderElement(node: ITreeNode, index: number, template: OutlineGroupTemplate): void { - template.label.set( - node.element.label, - createMatches(node.filterData) - ); + renderElement(node: ITreeNode, _index: number, template: DocumentSymbolGroupTemplate): void { + template.label.set(node.element.label, createMatches(node.filterData)); } - disposeTemplate(_template: OutlineGroupTemplate): void { + disposeTemplate(_template: DocumentSymbolGroupTemplate): void { // nothing } } -export class OutlineElementRenderer implements ITreeRenderer { +export class DocumentSymbolRenderer implements ITreeRenderer { - readonly templateId: string = OutlineElementTemplate.id; + readonly templateId: string = DocumentSymbolTemplate.id; constructor( + private _renderMarker: boolean, @IConfigurationService private readonly _configurationService: IConfigurationService, @IThemeService private readonly _themeService: IThemeService, ) { } - renderTemplate(container: HTMLElement): OutlineElementTemplate { + renderTemplate(container: HTMLElement): DocumentSymbolTemplate { container.classList.add('outline-element'); const iconLabel = new IconLabel(container, { supportHighlights: true }); const iconClass = dom.$('.outline-element-icon'); const decoration = dom.$('.outline-element-decoration'); container.prepend(iconClass); container.appendChild(decoration); - return new OutlineElementTemplate(container, iconLabel, iconClass, decoration); + return new DocumentSymbolTemplate(container, iconLabel, iconClass, decoration); } - renderElement(node: ITreeNode, index: number, template: OutlineElementTemplate): void { + renderElement(node: ITreeNode, _index: number, template: DocumentSymbolTemplate): void { const { element } = node; - const options = { + const options: IIconLabelValueOptions = { matches: createMatches(node.filterData), labelEscapeNewLines: true, - extraClasses: [], - title: localize('title.template', "{0} ({1})", element.symbol.name, OutlineElementRenderer._symbolKindNames[element.symbol.kind]) + extraClasses: ['nowrap'], + title: localize('title.template', "{0} ({1})", element.symbol.name, DocumentSymbolRenderer._symbolKindNames[element.symbol.kind]) }; if (this._configurationService.getValue(OutlineConfigKeys.icons)) { // add styles for the icons @@ -152,14 +144,17 @@ export class OutlineElementRenderer implements ITreeRenderer= 0) { - options.extraClasses.push(`deprecated`); + options.extraClasses!.push(`deprecated`); options.matches = []; } template.iconLabel.setLabel(element.symbol.name, element.symbol.detail, options); - this._renderMarkerInfo(element, template); + + if (this._renderMarker) { + this._renderMarkerInfo(element, template); + } } - private _renderMarkerInfo(element: OutlineElement, template: OutlineElementTemplate): void { + private _renderMarkerInfo(element: OutlineElement, template: DocumentSymbolTemplate): void { if (!element.marker) { dom.hide(template.decoration); @@ -227,47 +222,12 @@ export class OutlineElementRenderer implements ITreeRenderer { - - static readonly configNameToKind = Object.freeze({ - ['showFiles']: SymbolKind.File, - ['showModules']: SymbolKind.Module, - ['showNamespaces']: SymbolKind.Namespace, - ['showPackages']: SymbolKind.Package, - ['showClasses']: SymbolKind.Class, - ['showMethods']: SymbolKind.Method, - ['showProperties']: SymbolKind.Property, - ['showFields']: SymbolKind.Field, - ['showConstructors']: SymbolKind.Constructor, - ['showEnums']: SymbolKind.Enum, - ['showInterfaces']: SymbolKind.Interface, - ['showFunctions']: SymbolKind.Function, - ['showVariables']: SymbolKind.Variable, - ['showConstants']: SymbolKind.Constant, - ['showStrings']: SymbolKind.String, - ['showNumbers']: SymbolKind.Number, - ['showBooleans']: SymbolKind.Boolean, - ['showArrays']: SymbolKind.Array, - ['showObjects']: SymbolKind.Object, - ['showKeys']: SymbolKind.Key, - ['showNull']: SymbolKind.Null, - ['showEnumMembers']: SymbolKind.EnumMember, - ['showStructs']: SymbolKind.Struct, - ['showEvents']: SymbolKind.Event, - ['showOperators']: SymbolKind.Operator, - ['showTypeParameters']: SymbolKind.TypeParameter, - }); +export class DocumentSymbolFilter implements ITreeFilter { static readonly kindToConfigName = Object.freeze({ [SymbolKind.File]: 'showFiles', @@ -299,60 +259,48 @@ export class OutlineFilter implements ITreeFilter { }); constructor( - private readonly _prefix: string, + private readonly _prefix: 'breadcrumbs' | 'outline', @ITextResourceConfigurationService private readonly _textResourceConfigService: ITextResourceConfigurationService, ) { } - filter(element: OutlineItem): boolean { + filter(element: DocumentSymbolItem): boolean { const outline = OutlineModel.get(element); - let uri: URI | undefined; - - if (outline) { - uri = outline.uri; - } - if (!(element instanceof OutlineElement)) { return true; } - - const configName = OutlineFilter.kindToConfigName[element.symbol.kind]; + const configName = DocumentSymbolFilter.kindToConfigName[element.symbol.kind]; const configKey = `${this._prefix}.${configName}`; - return this._textResourceConfigService.getValue(uri, configKey); + return this._textResourceConfigService.getValue(outline?.uri, configKey); } } -export class OutlineItemComparator implements ITreeSorter { +export class DocumentSymbolComparator implements IOutlineComparator { private readonly _collator = new IdleValue(() => new Intl.Collator(undefined, { numeric: true })); - constructor( - public type: OutlineSortOrder = OutlineSortOrder.ByPosition - ) { } - - compare(a: OutlineItem, b: OutlineItem): number { + compareByPosition(a: DocumentSymbolItem, b: DocumentSymbolItem): number { if (a instanceof OutlineGroup && b instanceof OutlineGroup) { return a.order - b.order; - } else if (a instanceof OutlineElement && b instanceof OutlineElement) { - if (this.type === OutlineSortOrder.ByKind) { - return a.symbol.kind - b.symbol.kind || this._collator.value.compare(a.symbol.name, b.symbol.name); - } else if (this.type === OutlineSortOrder.ByName) { - return this._collator.value.compare(a.symbol.name, b.symbol.name) || Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range); - } else if (this.type === OutlineSortOrder.ByPosition) { - return Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range) || this._collator.value.compare(a.symbol.name, b.symbol.name); - } + return Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range) || this._collator.value.compare(a.symbol.name, b.symbol.name); } return 0; } -} - -export class OutlineDataSource implements IDataSource { - - getChildren(element: undefined | OutlineModel | OutlineGroup | OutlineElement) { - if (!element) { - return Iterable.empty(); + compareByType(a: DocumentSymbolItem, b: DocumentSymbolItem): number { + if (a instanceof OutlineGroup && b instanceof OutlineGroup) { + return a.order - b.order; + } else if (a instanceof OutlineElement && b instanceof OutlineElement) { + return a.symbol.kind - b.symbol.kind || this._collator.value.compare(a.symbol.name, b.symbol.name); } - return element.children.values(); + return 0; + } + compareByName(a: DocumentSymbolItem, b: DocumentSymbolItem): number { + if (a instanceof OutlineGroup && b instanceof OutlineGroup) { + return a.order - b.order; + } else if (a instanceof OutlineElement && b instanceof OutlineElement) { + return this._collator.value.compare(a.symbol.name, b.symbol.name) || Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range); + } + return 0; } } @@ -721,5 +669,4 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = if (symbolIconVariableColor) { collector.addRule(`${Codicon.symbolVariable.cssSelector} { color: ${symbolIconVariableColor}; }`); } - }); diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts index eb456674..99417655 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts @@ -30,10 +30,10 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv } private get configuration() { - const editorConfig = this.configurationService.getValue().workbench.editor; + const editorConfig = this.configurationService.getValue().workbench?.editor; return { - openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, + openEditorPinned: !editorConfig?.enablePreviewFromQuickOpen || !editorConfig?.enablePreview }; } @@ -44,12 +44,12 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv protected gotoLocation(context: IQuickAccessTextEditorContext, options: { range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean, preserveFocus?: boolean }): void { // Check for sideBySide use - if ((options.keyMods.ctrlCmd || options.forceSideBySide) && this.editorService.activeEditor) { + if ((options.keyMods.alt || (this.configuration.openEditorPinned && options.keyMods.ctrlCmd) || options.forceSideBySide) && this.editorService.activeEditor) { context.restoreViewState?.(); // since we open to the side, restore view state in this editor this.editorService.openEditor(this.editorService.activeEditor, { selection: options.range, - pinned: options.keyMods.alt || this.configuration.openEditorPinned, + pinned: options.keyMods.ctrlCmd || this.configuration.openEditorPinned, preserveFocus: options.preserveFocus }, SIDE_GROUP); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index bb7642fe..4f76c554 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -12,9 +12,9 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IQuickAccessRegistry, Extensions as QuickaccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from 'vs/editor/contrib/quickAccess/gotoSymbolQuickAccess'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkbenchEditorConfiguration, IEditorPane } from 'vs/workbench/common/editor'; +import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; import { ITextModel } from 'vs/editor/common/model'; -import { DisposableStore, IDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, toDisposable, Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { timeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; @@ -23,10 +23,11 @@ import { prepareQuery } from 'vs/base/common/fuzzyScorer'; import { SymbolKind } from 'vs/editor/common/modes'; import { fuzzyScore, createMatches } from 'vs/base/common/filters'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickAccessTextEditorContext } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess'; +import { IOutlineService, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; +import { isCompositeEditor } from 'vs/editor/browser/editorBrowser'; export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccessProvider { @@ -34,7 +35,8 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess constructor( @IEditorService private readonly editorService: IEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IOutlineService private readonly outlineService: IOutlineService, ) { super({ openSideBySideDirection: () => this.configuration.openSideBySideDirection @@ -43,36 +45,37 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess //#region DocumentSymbols (text editor required) - protected provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick, token: CancellationToken): IDisposable { - if (this.canPickFromTableOfContents()) { - return this.doGetTableOfContentsPicks(picker); - } - - return super.provideWithTextEditor(context, picker, token); - } - private get configuration() { - const editorConfig = this.configurationService.getValue().workbench.editor; + const editorConfig = this.configurationService.getValue().workbench?.editor; return { - openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, - openSideBySideDirection: editorConfig.openSideBySideDirection + openEditorPinned: !editorConfig?.enablePreviewFromQuickOpen || !editorConfig?.enablePreview, + openSideBySideDirection: editorConfig?.openSideBySideDirection }; } protected get activeTextEditorControl() { + // TODO@bpasero this distinction should go away by adopting `IOutlineService` + // for all editors (either text based ones or not). Currently text based + // editors are not yet using the new outline service infrastructure but the + // "classical" document symbols approach. + + if (isCompositeEditor(this.editorService.activeEditorPane?.getControl())) { + return undefined; + } + return this.editorService.activeTextEditorControl; } protected gotoLocation(context: IQuickAccessTextEditorContext, options: { range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean, preserveFocus?: boolean }): void { // Check for sideBySide use - if ((options.keyMods.ctrlCmd || options.forceSideBySide) && this.editorService.activeEditor) { + if ((options.keyMods.alt || (this.configuration.openEditorPinned && options.keyMods.ctrlCmd) || options.forceSideBySide) && this.editorService.activeEditor) { context.restoreViewState?.(); // since we open to the side, restore view state in this editor this.editorService.openEditor(this.editorService.activeEditor, { selection: options.range, - pinned: options.keyMods.alt || this.configuration.openEditorPinned, + pinned: options.keyMods.ctrlCmd || this.configuration.openEditorPinned, preserveFocus: options.preserveFocus }, SIDE_GROUP); } @@ -104,7 +107,7 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess return []; } - return this.doGetSymbolPicks(this.getDocumentSymbols(model, true, token), prepareQuery(filter), options, token); + return this.doGetSymbolPicks(this.getDocumentSymbols(model, token), prepareQuery(filter), options, token); } addDecorations(editor: IEditor, range: IRange): void { @@ -118,22 +121,21 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess //#endregion protected provideWithoutTextEditor(picker: IQuickPick): IDisposable { - if (this.canPickFromTableOfContents()) { - return this.doGetTableOfContentsPicks(picker); + if (this.canPickWithOutlineService()) { + return this.doGetOutlinePicks(picker); } return super.provideWithoutTextEditor(picker); } - private canPickFromTableOfContents(): boolean { - return this.editorService.activeEditorPane ? TableOfContentsProviderRegistry.has(this.editorService.activeEditorPane.getId()) : false; + private canPickWithOutlineService(): boolean { + return this.editorService.activeEditorPane ? this.outlineService.canCreateOutline(this.editorService.activeEditorPane) : false; } - private doGetTableOfContentsPicks(picker: IQuickPick): IDisposable { + private doGetOutlinePicks(picker: IQuickPick): IDisposable { const pane = this.editorService.activeEditorPane; if (!pane) { return Disposable.None; } - const provider = TableOfContentsProviderRegistry.get(pane.getId())!; const cts = new CancellationTokenSource(); const disposables = new DisposableStore(); @@ -141,30 +143,45 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess picker.busy = true; - provider.provideTableOfContents(pane, { disposables }, cts.token).then(entries => { + this.outlineService.createOutline(pane, OutlineTarget.QuickPick, cts.token).then(outline => { - picker.busy = false; - - if (cts.token.isCancellationRequested || !entries || entries.length === 0) { + if (!outline) { return; } + if (cts.token.isCancellationRequested) { + outline.dispose(); + return; + } + + disposables.add(outline); + + const viewState = outline.captureViewState(); + disposables.add(toDisposable(() => { + if (picker.selectedItems.length === 0) { + viewState.dispose(); + } + })); + + const entries = Array.from(outline.config.quickPickDataSource.getQuickPickElements()); const items: IGotoSymbolQuickPickItem[] = entries.map((entry, idx) => { return { kind: SymbolKind.File, index: idx, score: 0, - label: entry.icon ? `$(${entry.icon.id}) ${entry.label}` : entry.label, - ariaLabel: entry.detail ? `${entry.label}, ${entry.detail}` : entry.label, - detail: entry.detail, + label: entry.label, description: entry.description, + ariaLabel: entry.ariaLabel, + iconClasses: entry.iconClasses }; }); disposables.add(picker.onDidAccept(() => { picker.hide(); const [entry] = picker.selectedItems; - entries[entry.index]?.pick(); + if (entry && entries[entry.index]) { + outline.reveal(entries[entry.index].element, {}, false); + } })); const updatePickerItems = () => { @@ -194,16 +211,23 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess updatePickerItems(); disposables.add(picker.onDidChangeValue(updatePickerItems)); + const previewDisposable = new MutableDisposable(); + disposables.add(previewDisposable); + disposables.add(picker.onDidChangeActive(() => { const [entry] = picker.activeItems; - if (entry) { - entries[entry.index]?.preview(); + if (entry && entries[entry.index]) { + previewDisposable.value = outline.preview(entries[entry.index].element); + } else { + previewDisposable.clear(); } })); }).catch(err => { onUnexpectedError(err); picker.hide(); + }).finally(() => { + picker.busy = false; }); return disposables; @@ -243,45 +267,3 @@ registerAction2(class GotoSymbolAction extends Action2 { accessor.get(IQuickInputService).quickAccess.show(GotoSymbolQuickAccessProvider.PREFIX); } }); - -//#region toc definition and logic - -export interface ITableOfContentsEntry { - icon?: ThemeIcon; - label: string; - detail?: string; - description?: string; - pick(): any; - preview(): any; -} - -export interface ITableOfContentsProvider { - - provideTableOfContents(editor: T, context: { disposables: DisposableStore }, token: CancellationToken): Promise; -} - -class ProviderRegistry { - - private readonly _provider = new Map(); - - register(type: string, provider: ITableOfContentsProvider): IDisposable { - this._provider.set(type, provider); - return toDisposable(() => { - if (this._provider.get(type) === provider) { - this._provider.delete(type); - } - }); - } - - get(type: string): ITableOfContentsProvider | undefined { - return this._provider.get(type); - } - - has(type: string): boolean { - return this._provider.has(type); - } -} - -export const TableOfContentsProviderRegistry = new ProviderRegistry(); - -//#endregion diff --git a/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts index f7e084d0..4e693565 100644 --- a/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts @@ -42,25 +42,25 @@ suite('Save Participants', function () { let lineContent = ''; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), lineContent); + assert.strictEqual(snapshotToString(model.createSnapshot()!), lineContent); // No new line if last line already empty lineContent = `Hello New Line${model.textEditorModel.getEOL()}`; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), lineContent); + assert.strictEqual(snapshotToString(model.createSnapshot()!), lineContent); // New empty line added (single line) lineContent = 'Hello New Line'; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), `${lineContent}${model.textEditorModel.getEOL()}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${lineContent}${model.textEditorModel.getEOL()}`); // New empty line added (multi line) lineContent = `Hello New Line${model.textEditorModel.getEOL()}Hello New Line${model.textEditorModel.getEOL()}Hello New Line`; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), `${lineContent}${model.textEditorModel.getEOL()}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${lineContent}${model.textEditorModel.getEOL()}`); }); test('trim final new lines', async function () { @@ -77,25 +77,25 @@ suite('Save Participants', function () { let lineContent = `${textContent}`; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), lineContent); + assert.strictEqual(snapshotToString(model.createSnapshot()!), lineContent); // No new line removal if last line is single new line lineContent = `${textContent}${eol}`; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), lineContent); + assert.strictEqual(snapshotToString(model.createSnapshot()!), lineContent); // Remove new line (single line with two new lines) lineContent = `${textContent}${eol}${eol}`; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}${eol}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}${eol}`); // Remove new lines (multiple lines with multiple new lines) lineContent = `${textContent}${eol}${textContent}${eol}${eol}${eol}`; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}${eol}${textContent}${eol}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}${eol}${textContent}${eol}`); }); test('trim final new lines bug#39750', async function () { @@ -117,12 +117,12 @@ suite('Save Participants', function () { // undo await model.textEditorModel.undo(); - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}`); // trim final new lines should not mess the undo stack await participant.participate(model, { reason: SaveReason.EXPLICIT }); await model.textEditorModel.redo(); - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}.`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}.`); }); test('trim final new lines bug#46075', async function () { @@ -143,13 +143,13 @@ suite('Save Participants', function () { } // confirm trimming - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}${eol}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}${eol}`); // undo should go back to previous content immediately await model.textEditorModel.undo(); - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}${eol}${eol}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}${eol}${eol}`); await model.textEditorModel.redo(); - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}${eol}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}${eol}`); }); test('trim whitespace', async function () { @@ -169,6 +169,6 @@ suite('Save Participants', function () { } // confirm trimming - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}`); }); }); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index ffab7e07..1d4e0e38 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -25,7 +25,7 @@ import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; import { peekViewBorder } from 'vs/editor/contrib/peekView/peekView'; import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/zoneWidget'; import * as nls from 'vs/nls'; -import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenu, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -45,7 +45,6 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; -import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { PANEL_BORDER } from 'vs/workbench/common/theme'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { Codicon } from 'vs/base/common/codicons'; @@ -239,15 +238,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget const actionsContainer = dom.append(this._headElement, dom.$('.review-actions')); this._actionbarWidget = new ActionBar(actionsContainer, { - actionViewItemProvider: (action: IAction) => { - if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action); - } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); - } else { - return new ActionViewItem({}, action, { label: false, icon: true }); - } - } + actionViewItemProvider: createActionViewItem.bind(undefined, this.instantiationService) }); this._disposables.add(this._actionbarWidget); diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 9ddb42f7..0249effb 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -142,6 +142,7 @@ export class CommentNodeRenderer implements IListRenderer } templateData.commentText.appendChild(renderedComment); + templateData.commentText.title = renderedComment.textContent ?? ''; } disposeTemplate(templateData: ICommentThreadTemplateData): void { diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index 5ed011a2..ec5544fd 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -6,11 +6,9 @@ import 'vs/css!./media/panel'; import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; -import { basename, isEqual } from 'vs/base/common/resources'; -import { IAction, Action } from 'vs/base/common/actions'; -import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults'; +import { basename } from 'vs/base/common/resources'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { CommentNode, CommentsModel, ResourceWithCommentThreads, ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel'; import { CommentController } from 'vs/workbench/contrib/comments/browser/commentsEditorContribution'; @@ -20,14 +18,19 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { textLinkForeground, textLinkActiveForeground, focusBorder, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry'; import { ResourceLabels } from 'vs/workbench/browser/labels'; import { CommentsList, COMMENTS_VIEW_ID, COMMENTS_VIEW_TITLE } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewDescriptorService, IViewsService } from 'vs/workbench/common/views'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyAndExpr, ContextKeyEqualsExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Codicon } from 'vs/base/common/codicons'; + +const CONTEXT_KEY_HAS_COMMENTS = new RawContextKey('commentsView.hasComments', false); export class CommentsPanel extends ViewPane { private treeLabels!: ResourceLabels; @@ -35,7 +38,7 @@ export class CommentsPanel extends ViewPane { private treeContainer!: HTMLElement; private messageBoxContainer!: HTMLElement; private commentsModel!: CommentsModel; - private collapseAllAction?: IAction; + private readonly hasCommentsContextKey: IContextKey; readonly onDidChangeVisibility = this.onDidChangeBodyVisibility; @@ -52,8 +55,10 @@ export class CommentsPanel extends ViewPane { @IThemeService themeService: IThemeService, @ICommentService private readonly commentService: ICommentService, @ITelemetryService telemetryService: ITelemetryService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + this.hasCommentsContextKey = CONTEXT_KEY_HAS_COMMENTS.bindTo(contextKeyService); } public renderBody(container: HTMLElement): void { @@ -129,13 +134,14 @@ export class CommentsPanel extends ViewPane { await this.tree.setInput(this.commentsModel); } - public getActions(): IAction[] { - if (!this.collapseAllAction) { - this.collapseAllAction = new Action('vs.tree.collapse', nls.localize('collapseAll', "Collapse All"), 'collapse-all', true, () => this.tree ? new CollapseAllAction(this.tree, true).run() : Promise.resolve()); - this._register(this.collapseAllAction); + public collapseAll() { + if (this.tree) { + this.tree.collapseAll(); + this.tree.setSelection([]); + this.tree.setFocus([]); + this.tree.domFocus(); + this.tree.focusFirst(); } - - return [this.collapseAllAction]; } public layoutBody(height: number, width: number): void { @@ -206,7 +212,7 @@ export class CommentsPanel extends ViewPane { const activeEditor = this.editorService.activeEditor; let currentActiveResource = activeEditor ? activeEditor.resource : undefined; - if (currentActiveResource && isEqual(currentActiveResource, element.resource)) { + if (this.uriIdentityService.extUri.isEqual(element.resource, currentActiveResource)) { const threadToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].threadId : element.threadId; const commentToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].comment.uniqueIdInThread : element.comment.uniqueIdInThread; const control = this.editorService.activeTextEditorControl; @@ -243,9 +249,7 @@ export class CommentsPanel extends ViewPane { private async refresh(): Promise { if (this.isVisible()) { - if (this.collapseAllAction) { - this.collapseAllAction.enabled = this.commentsModel.hasCommentThreads(); - } + this.hasCommentsContextKey.set(this.commentsModel.hasCommentThreads()); this.treeContainer.classList.toggle('hidden', !this.commentsModel.hasCommentThreads()); this.renderMessage(); @@ -281,3 +285,23 @@ CommandsRegistry.registerCommand({ viewsService.openView(COMMENTS_VIEW_ID, true); } }); + +registerAction2(class Collapse extends ViewAction { + constructor() { + super({ + viewId: COMMENTS_VIEW_ID, + id: 'comments.collapse', + title: nls.localize('collapseAll', "Collapse All"), + f1: false, + icon: Codicon.collapseAll, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyAndExpr.create([ContextKeyEqualsExpr.create('view', COMMENTS_VIEW_ID), CONTEXT_KEY_HAS_COMMENTS]) + } + }); + } + runInView(_accessor: ServicesAccessor, view: CommentsPanel) { + view.collapseAll(); + } +}); diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index c57aa6dd..66de7e83 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce, distinct } from 'vs/base/common/arrays'; +import { coalesce, distinct, firstOrDefault } from 'vs/base/common/arrays'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { Lazy } from 'vs/base/common/lazy'; @@ -28,7 +28,7 @@ import { EditorInput, EditorOptions, Extensions as EditorInputExtensions, GroupI import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { CONTEXT_CUSTOM_EDITORS, CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CustomEditorCapabilities, CustomEditorInfo, CustomEditorInfoCollection, CustomEditorPriority, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { CustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditorModelManager'; -import { IWebviewService, webviewHasOwnEditFunctionsContext } from 'vs/workbench/contrib/webview/browser/webview'; +import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CustomEditorAssociation, CustomEditorsAssociations, customEditorsAssociationsSettingId } from 'vs/workbench/services/editor/common/editorOpenWith'; import { ICustomEditorInfo, ICustomEditorViewTypesHandler, IEditorService, IOpenEditorOverride, IOpenEditorOverrideEntry } from 'vs/workbench/services/editor/common/editorService'; @@ -45,7 +45,6 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ private readonly _customEditorContextKey: IContextKey; private readonly _focusedCustomEditorIsEditable: IContextKey; - private readonly _webviewHasOwnEditFunctions: IContextKey; private readonly _onDidChangeViewTypes = new Emitter(); onDidChangeViewTypes: Event = this._onDidChangeViewTypes.event; @@ -66,7 +65,6 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ this._customEditorContextKey = CONTEXT_CUSTOM_EDITORS.bindTo(contextKeyService); this._focusedCustomEditorIsEditable = CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE.bindTo(contextKeyService); - this._webviewHasOwnEditFunctions = webviewHasOwnEditFunctionsContext.bindTo(contextKeyService); this._contributedEditors = this._register(new ContributedCustomEditors(storageService)); this._register(this._contributedEditors.onChange(() => { @@ -156,13 +154,8 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ ...this.getAllCustomEditors(resource).allEditors, ]); - let currentlyOpenedEditorType: undefined | string; - for (const editor of group ? group.editors : []) { - if (editor.resource && isEqual(editor.resource, resource)) { - currentlyOpenedEditorType = editor instanceof CustomEditorInput ? editor.viewType : defaultCustomEditor.id; - break; - } - } + const existingEditorForResource = group && firstOrDefault(this.editorService.findEditors(resource, group)); + const currentlyOpenedEditorType: undefined | string = existingEditorForResource instanceof CustomEditorInput ? existingEditorForResource.viewType : defaultCustomEditor.id; const resourceExt = extname(resource); @@ -278,9 +271,8 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ } // Try to replace existing editors for resource - const existingEditors = targetGroup.editors.filter(editor => editor.resource && isEqual(editor.resource, resource)); - if (existingEditors.length) { - const existing = existingEditors[0]; + const existing = firstOrDefault(this.editorService.findEditors(resource, targetGroup)); + if (existing) { if (!input.matches(existing)) { await this.editorService.replaceEditors([{ editor: existing, @@ -317,7 +309,6 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ if (!resource) { this._customEditorContextKey.reset(); this._focusedCustomEditorIsEditable.reset(); - this._webviewHasOwnEditFunctions.reset(); return; } @@ -325,7 +316,6 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ this._customEditorContextKey.set(possibleEditors.map(x => x.id).join(',')); this._focusedCustomEditorIsEditable.set(activeEditorPane?.input instanceof CustomEditorInput); - this._webviewHasOwnEditFunctions.set(possibleEditors.length > 0); } private async handleMovedFileInOpenedFileEditors(oldResource: URI, newResource: URI): Promise { @@ -457,7 +447,7 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo return this.onEditorOpening(editor, options, group); }, getEditorOverrides: (resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined): IOpenEditorOverrideEntry[] => { - const currentEditor = group?.editors.find(editor => isEqual(editor.resource, resource)); + const currentEditor = group && firstOrDefault(this.editorService.findEditors(resource, group)); const toOverride = (entry: CustomEditorInfo): IOpenEditorOverrideEntry => { return { @@ -546,7 +536,7 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo return; } - const existingEditorForResource = group.editors.find(editor => isEqual(resource, editor.resource)); + const existingEditorForResource = firstOrDefault(this.editorService.findEditors(resource, group)); if (existingEditorForResource) { if (editor === existingEditorForResource) { return; diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 966dc83e..65d81d07 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -15,7 +15,6 @@ import { IModelDecorationOptions, IModelDeltaDecoration, TrackedRangeStickiness, 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, IBreakpointEditorContribution, IBreakpointUpdateData, IDebugConfiguration, State, IDebugSession } from 'vs/workbench/contrib/debug/common/debug'; import { IMarginData } from 'vs/editor/browser/controller/mouseTarget'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -173,6 +172,24 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi this.setDecorationsScheduler.schedule(); } + /** + * Returns context menu actions at the line number if breakpoints can be + * set. This is used by the {@link TestingDecorations} to allow breakpoint + * setting on lines where breakpoint "run" actions are present. + */ + public getContextMenuActionsAtPosition(lineNumber: number, model: ITextModel) { + if (!this.debugService.getAdapterManager().hasDebuggers()) { + return []; + } + + if (!this.debugService.canSetBreakpointsIn(model)) { + return []; + } + + const breakpoints = this.debugService.getModel().getBreakpoints({ lineNumber, uri: model.uri }); + return this.getContextMenuActions(breakpoints, model.uri, lineNumber); + } + private registerListeners(): void { this.toDispose.push(this.editor.onMouseDown(async (e: IEditorMouseEvent) => { if (!this.debugService.getAdapterManager().hasDebuggers()) { @@ -301,7 +318,9 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi const actions: IAction[] = []; if (breakpoints.length === 1) { const breakpointType = breakpoints[0].logMessage ? nls.localize('logPoint', "Logpoint") : nls.localize('breakpoint', "Breakpoint"); - actions.push(new RemoveBreakpointAction(RemoveBreakpointAction.ID, nls.localize('removeBreakpoint', "Remove {0}", breakpointType), this.debugService)); + actions.push(new Action('debug.removeBreakpoint', nls.localize('removeBreakpoint', "Remove {0}", breakpointType), undefined, true, async () => { + await this.debugService.removeBreakpoints(breakpoints[0].getId()); + })); actions.push(new Action( 'workbench.debug.action.editBreakpointAction', nls.localize('editBreakpoint', "Edit {0}...", breakpointType), @@ -375,7 +394,8 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi const decorations = this.editor.getLineDecorations(line); if (decorations) { for (const { options } of decorations) { - if (options.glyphMarginClassName && options.glyphMarginClassName.indexOf('codicon-') === -1) { + const clz = options.glyphMarginClassName; + if (clz && (!clz.includes('codicon-') || clz.includes('codicon-testing-'))) { return false; } } @@ -454,7 +474,7 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi // 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 icon = candidate.breakpoint ? getBreakpointMessageAndIcon(this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), candidate.breakpoint, this.labelService).icon : icons.debugBreakpointDisabled; + const icon = candidate.breakpoint ? getBreakpointMessageAndIcon(this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), candidate.breakpoint, this.labelService).icon : icons.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, ThemeIcon.asClassName(icon), candidate.breakpoint, this.debugService, this.contextMenuService, contextMenuActions); @@ -645,15 +665,11 @@ registerThemingParticipant((theme, collector) => { const debugIconBreakpointColor = theme.getColor(debugIconBreakpointForeground); if (debugIconBreakpointColor) { collector.addRule(` - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpoint)}, - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointConditional)}, - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointLog)}, - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointFunction)}, - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointData)}, + ${icons.allBreakpoints.map(b => `.monaco-workbench ${ThemeIcon.asCSSSelector(b.regular)}`).join(',\n ')}, .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointUnsupported)}, .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointHint)}:not([class*='codicon-debug-breakpoint']):not([class*='codicon-debug-stackframe']), - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpoint)}${ThemeIcon.asCSSSelector(icons.debugStackframeFocused)}::after, - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpoint)}${ThemeIcon.asCSSSelector(icons.debugStackframe)}::after { + .monaco-workbench ${ThemeIcon.asCSSSelector(icons.breakpoint.regular)}${ThemeIcon.asCSSSelector(icons.debugStackframeFocused)}::after, + .monaco-workbench ${ThemeIcon.asCSSSelector(icons.breakpoint.regular)}${ThemeIcon.asCSSSelector(icons.debugStackframe)}::after { color: ${debugIconBreakpointColor} !important; } `); @@ -662,7 +678,7 @@ registerThemingParticipant((theme, collector) => { const debugIconBreakpointDisabledColor = theme.getColor(debugIconBreakpointDisabledForeground); if (debugIconBreakpointDisabledColor) { collector.addRule(` - .monaco-workbench .codicon[class*='-disabled'] { + ${icons.allBreakpoints.map(b => `.monaco-workbench ${ThemeIcon.asCSSSelector(b.disabled)}`).join(',\n ')} { color: ${debugIconBreakpointDisabledColor} !important; } `); @@ -671,7 +687,7 @@ registerThemingParticipant((theme, collector) => { const debugIconBreakpointUnverifiedColor = theme.getColor(debugIconBreakpointUnverifiedForeground); if (debugIconBreakpointUnverifiedColor) { collector.addRule(` - .monaco-workbench .codicon[class*='-unverified'] { + ${icons.allBreakpoints.map(b => `.monaco-workbench ${ThemeIcon.asCSSSelector(b.unverified)}`).join(',\n ')} { color: ${debugIconBreakpointUnverifiedColor}; } `); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts index 362d0544..cc8b3963 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts @@ -221,6 +221,9 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi this.input = scopedInstatiationService.createInstance(CodeEditorWidget, container, options, codeEditorWidgetOptions); CONTEXT_IN_BREAKPOINT_WIDGET.bindTo(scopedContextKeyService).set(true); const model = this.modelService.createModel('', null, uri.parse(`${DEBUG_SCHEME}:${this.editor.getId()}:breakpointinput`), true); + if (this.editor.hasModel()) { + model.setMode(this.editor.getModel().getLanguageIdentifier()); + } this.input.setModel(model); this.toDispose.push(model); const setDecorations = () => { diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 7fa72c43..4fb3fead 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -3,13 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -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, Separator } from 'vs/base/common/actions'; -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 { IAction } from 'vs/base/common/actions'; +import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINTS_FOCUSED, State, DEBUG_SCHEME, IFunctionBreakpoint, IExceptionBreakpoint, IEnablement, IDebugModel, IDataBreakpoint, BREAKPOINTS_VIEW_ID, CONTEXT_BREAKPOINT_ITEM_TYPE, CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_IN_DEBUG_MODE, IBaseBreakpoint, IBreakpointEditorContribution, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINT_INPUT_FOCUSED } 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'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -24,12 +22,11 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { WorkbenchList, ListResourceNavigator } from 'vs/platform/list/browser/listService'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; -import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { ILabelService } from 'vs/platform/label/common/label'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, ContextKeyEqualsExpr, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { Gesture } from 'vs/base/browser/touch'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; @@ -38,6 +35,13 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { registerAction2, Action2, MenuId, IMenu, IMenuService } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { createAndFillInContextMenuActions, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Codicon } from 'vs/base/common/codicons'; const $ = dom.$; @@ -57,11 +61,21 @@ export function getExpandedBodySize(model: IDebugModel, countLimit: number): num } type BreakpointItem = IBreakpoint | IFunctionBreakpoint | IDataBreakpoint | IExceptionBreakpoint; +interface InputBoxData { + breakpoint: IFunctionBreakpoint | IExceptionBreakpoint; + type: 'condition' | 'hitCount' | 'name'; +} + export class BreakpointsView extends ViewPane { private list!: WorkbenchList; private needsRefresh = false; private ignoreLayout = false; + private menu: IMenu; + private breakpointItemType: IContextKey; + private breakpointSupportsCondition: IContextKey; + private _inputBoxData: InputBoxData | undefined; + breakpointInputFocused: IContextKey; constructor( options: IViewletViewOptions, @@ -78,26 +92,32 @@ export class BreakpointsView extends ViewPane { @IOpenerService openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService, @ILabelService private readonly labelService: ILabelService, + @IMenuService menuService: IMenuService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + this.menu = menuService.createMenu(MenuId.DebugBreakpointsContext, contextKeyService); + this._register(this.menu); + this.breakpointItemType = CONTEXT_BREAKPOINT_ITEM_TYPE.bindTo(contextKeyService); + this.breakpointSupportsCondition = CONTEXT_BREAKPOINT_SUPPORTS_CONDITION.bindTo(contextKeyService); + this.breakpointInputFocused = CONTEXT_BREAKPOINT_INPUT_FOCUSED.bindTo(contextKeyService); this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.onBreakpointsChange())); } - public renderBody(container: HTMLElement): void { + renderBody(container: HTMLElement): void { super.renderBody(container); this.element.classList.add('debug-pane'); container.classList.add('debug-breakpoints'); - const delegate = new BreakpointsDelegate(this.debugService); + const delegate = new BreakpointsDelegate(this); this.list = >this.instantiationService.createInstance(WorkbenchList, 'Breakpoints', container, delegate, [ - this.instantiationService.createInstance(BreakpointsRenderer), - new ExceptionBreakpointsRenderer(this.debugService), - new ExceptionBreakpointInputRenderer(this.debugService, this.contextViewService, this.themeService), - this.instantiationService.createInstance(FunctionBreakpointsRenderer), + this.instantiationService.createInstance(BreakpointsRenderer, this.menu, this.breakpointSupportsCondition), + new ExceptionBreakpointsRenderer(this.menu, this.breakpointSupportsCondition, this.debugService), + new ExceptionBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.themeService), + this.instantiationService.createInstance(FunctionBreakpointsRenderer, this.menu, this.breakpointSupportsCondition), this.instantiationService.createInstance(DataBreakpointsRenderer), - new FunctionBreakpointInputRenderer(this.debugService, this.contextViewService, this.themeService, this.labelService) + new FunctionBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.themeService, this.labelService) ], { identityProvider: { getId: (element: IEnablement) => element.getId() }, multipleSelectionSupport: false, @@ -135,10 +155,9 @@ export class BreakpointsView extends ViewPane { if (e.element instanceof Breakpoint) { openBreakpointSource(e.element, e.sideBySide, e.editorOptions.preserveFocus || false, e.editorOptions.pinned || !e.editorOptions.preserveFocus, this.debugService, this.editorService); } - if (e.browserEvent instanceof MouseEvent && e.browserEvent.detail === 2 && e.element instanceof FunctionBreakpoint && e.element !== this.debugService.getViewModel().getSelectedBreakpoint()) { + if (e.browserEvent instanceof MouseEvent && e.browserEvent.detail === 2 && e.element instanceof FunctionBreakpoint && e.element !== this.inputBoxData?.breakpoint) { // double click - this.debugService.getViewModel().setSelectedBreakpoint(e.element); - this.onBreakpointsChange(); + this.renderInputBox({ breakpoint: e.element, type: 'name' }); } })); @@ -156,13 +175,23 @@ export class BreakpointsView extends ViewPane { })); } - public focus(): void { + focus(): void { super.focus(); if (this.list) { this.list.domFocus(); } } + renderInputBox(data: InputBoxData | undefined): void { + this._inputBoxData = data; + this.onBreakpointsChange(); + this._inputBoxData = undefined; + } + + get inputBoxData(): InputBoxData | undefined { + return this._inputBoxData; + } + protected layoutBody(height: number, width: number): void { if (this.ignoreLayout) { return; @@ -181,71 +210,25 @@ export class BreakpointsView extends ViewPane { } private onListContextMenu(e: IListContextMenuEvent): void { - if (!e.element) { - return; - } - - const actions: IAction[] = []; const element = e.element; + const type = element instanceof Breakpoint ? 'breakpoint' : element instanceof ExceptionBreakpoint ? 'exceptionBreakpoint' : + element instanceof FunctionBreakpoint ? 'functionBreakpoint' : element instanceof DataBreakpoint ? 'dataBreakpoint' : undefined; + this.breakpointItemType.set(type); + const session = this.debugService.getViewModel().focusedSession; + const conditionSupported = element instanceof ExceptionBreakpoint ? element.supportsCondition : (!session || !!session.capabilities.supportsConditionalBreakpoints); + this.breakpointSupportsCondition.set(conditionSupported); - if (element instanceof ExceptionBreakpoint) { - if (element.supportsCondition) { - actions.push(new Action('workbench.action.debug.editExceptionBreakpointCondition', nls.localize('editCondition', "Edit Condition"), '', true, async () => { - this.debugService.getViewModel().setSelectedBreakpoint(element); - this.onBreakpointsChange(); - })); - } - } else { - const breakpointType = element instanceof Breakpoint && element.logMessage ? nls.localize('Logpoint', "Logpoint") : nls.localize('Breakpoint', "Breakpoint"); - if (element instanceof Breakpoint || element instanceof FunctionBreakpoint) { - actions.push(new Action('workbench.action.debug.openEditorAndEditBreakpoint', nls.localize('editBreakpoint', "Edit {0}...", breakpointType), '', true, async () => { - if (element instanceof Breakpoint) { - const editor = await openBreakpointSource(element, false, false, true, this.debugService, this.editorService); - if (editor) { - const codeEditor = editor.getControl(); - if (isCodeEditor(codeEditor)) { - codeEditor.getContribution(BREAKPOINT_EDITOR_CONTRIBUTION_ID).showBreakpointWidget(element.lineNumber, element.column); - } - } - } else { - this.debugService.getViewModel().setSelectedBreakpoint(element); - this.onBreakpointsChange(); - } - })); - actions.push(new Separator()); - } - - - actions.push(new RemoveBreakpointAction(RemoveBreakpointAction.ID, nls.localize('removeBreakpoint', "Remove {0}", breakpointType), this.debugService)); - - if (this.debugService.getModel().getBreakpoints().length + this.debugService.getModel().getFunctionBreakpoints().length >= 1) { - actions.push(new RemoveAllBreakpointsAction(RemoveAllBreakpointsAction.ID, RemoveAllBreakpointsAction.LABEL, this.debugService, this.keybindingService)); - actions.push(new Separator()); - - actions.push(new EnableAllBreakpointsAction(EnableAllBreakpointsAction.ID, EnableAllBreakpointsAction.LABEL, this.debugService, this.keybindingService)); - actions.push(new DisableAllBreakpointsAction(DisableAllBreakpointsAction.ID, DisableAllBreakpointsAction.LABEL, this.debugService, this.keybindingService)); - } - - actions.push(new Separator()); - actions.push(new ReapplyBreakpointsAction(ReapplyBreakpointsAction.ID, ReapplyBreakpointsAction.LABEL, this.debugService, this.keybindingService)); - } + const secondary: IAction[] = []; + const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: e.element, shouldForwardArgs: false }, { primary: [], secondary }, g => /^inline/.test(g)); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, - getActions: () => actions, + getActions: () => secondary, getActionsContext: () => element, - onHide: () => dispose(actions) + onHide: () => dispose(actionsDisposable) }); } - public getActions(): IAction[] { - return [ - new AddFunctionBreakpointAction(AddFunctionBreakpointAction.ID, AddFunctionBreakpointAction.LABEL, this.debugService, this.keybindingService), - new ToggleBreakpointsActivatedAction(ToggleBreakpointsActivatedAction.ID, ToggleBreakpointsActivatedAction.ACTIVATE_LABEL, this.debugService, this.keybindingService), - new RemoveAllBreakpointsAction(RemoveAllBreakpointsAction.ID, RemoveAllBreakpointsAction.LABEL, this.debugService, this.keybindingService) - ]; - } - private updateSize(): void { const containerModel = this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerByViewId(this.id)!)!; @@ -282,7 +265,7 @@ export class BreakpointsView extends ViewPane { class BreakpointsDelegate implements IListVirtualDelegate { - constructor(private debugService: IDebugService) { + constructor(private view: BreakpointsView) { // noop } @@ -295,16 +278,16 @@ class BreakpointsDelegate implements IListVirtualDelegate { return BreakpointsRenderer.ID; } if (element instanceof FunctionBreakpoint) { - const selected = this.debugService.getViewModel().getSelectedBreakpoint(); - if (!element.name || (selected && selected.getId() === element.getId())) { + const inputBoxBreakpoint = this.view.inputBoxData?.breakpoint; + if (!element.name || (inputBoxBreakpoint && inputBoxBreakpoint.getId() === element.getId())) { return FunctionBreakpointInputRenderer.ID; } return FunctionBreakpointsRenderer.ID; } if (element instanceof ExceptionBreakpoint) { - const selected = this.debugService.getViewModel().getSelectedBreakpoint(); - if (selected && selected.getId() === element.getId()) { + const inputBoxBreakpoint = this.view.inputBoxData?.breakpoint; + if (inputBoxBreakpoint && inputBoxBreakpoint.getId() === element.getId()) { return ExceptionBreakpointInputRenderer.ID; } return ExceptionBreakpointsRenderer.ID; @@ -322,7 +305,9 @@ interface IBaseBreakpointTemplateData { name: HTMLElement; checkbox: HTMLInputElement; context: BreakpointItem; + actionBar: ActionBar; toDispose: IDisposable[]; + elementDisposable: IDisposable[]; } interface IBaseBreakpointWithIconTemplateData extends IBaseBreakpointTemplateData { @@ -338,26 +323,31 @@ interface IExceptionBreakpointTemplateData extends IBaseBreakpointTemplateData { condition: HTMLElement; } +interface IFunctionBreakpointTemplateData extends IBaseBreakpointWithIconTemplateData { + condition: HTMLElement; +} + interface IFunctionBreakpointInputTemplateData { inputBox: InputBox; checkbox: HTMLInputElement; icon: HTMLElement; breakpoint: IFunctionBreakpoint; - reactedOnEvent: boolean; toDispose: IDisposable[]; + type: 'hitCount' | 'condition' | 'name'; } interface IExceptionBreakpointInputTemplateData { inputBox: InputBox; checkbox: HTMLInputElement; breakpoint: IExceptionBreakpoint; - reactedOnEvent: boolean; toDispose: IDisposable[]; } class BreakpointsRenderer implements IListRenderer { constructor( + private menu: IMenu, + private breakpointSupportsCondition: IContextKey, @IDebugService private readonly debugService: IDebugService, @ILabelService private readonly labelService: ILabelService ) { @@ -377,6 +367,7 @@ class BreakpointsRenderer implements IListRenderer { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); @@ -387,6 +378,8 @@ class BreakpointsRenderer implements IListRenderer /^inline/.test(g))); + data.actionBar.clear(); + data.actionBar.push(primary, { icon: true, label: false }); + } + + disposeElement(_element: IBreakpoint, _index: number, templateData: IBreakpointTemplateData): void { + dispose(templateData.elementDisposable); } disposeTemplate(templateData: IBreakpointTemplateData): void { @@ -423,6 +427,8 @@ class BreakpointsRenderer implements IListRenderer { constructor( + private menu: IMenu, + private breakpointSupportsCondition: IContextKey, private debugService: IDebugService ) { // noop @@ -440,6 +446,7 @@ class ExceptionBreakpointsRenderer implements IListRenderer { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); @@ -450,6 +457,8 @@ class ExceptionBreakpointsRenderer implements IListRenderer /^inline/.test(g))); + data.actionBar.clear(); + data.actionBar.push(primary, { icon: true, label: false }); + } + + disposeElement(_element: IExceptionBreakpoint, _index: number, templateData: IExceptionBreakpointTemplateData): void { + dispose(templateData.elementDisposable); } disposeTemplate(templateData: IExceptionBreakpointTemplateData): void { @@ -467,9 +486,11 @@ class ExceptionBreakpointsRenderer implements IListRenderer { +class FunctionBreakpointsRenderer implements IListRenderer { constructor( + private menu: IMenu, + private breakpointSupportsCondition: IContextKey, @IDebugService private readonly debugService: IDebugService, @ILabelService private readonly labelService: ILabelService ) { @@ -482,13 +503,14 @@ class FunctionBreakpointsRenderer implements IListRenderer { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); @@ -497,11 +519,15 @@ class FunctionBreakpointsRenderer implements IListRenderer /^inline/.test(g))); + data.actionBar.clear(); + data.actionBar.push(primary, { icon: true, label: false }); } - disposeTemplate(templateData: IBaseBreakpointWithIconTemplateData): void { + disposeElement(_element: IFunctionBreakpoint, _index: number, templateData: IFunctionBreakpointTemplateData): void { + dispose(templateData.elementDisposable); + } + + disposeTemplate(templateData: IFunctionBreakpointTemplateData): void { dispose(templateData.toDispose); } } @@ -570,7 +611,7 @@ class DataBreakpointsRenderer implements IListRenderer { constructor( + private view: BreakpointsView, private debugService: IDebugService, private contextViewService: IContextViewService, private themeService: IThemeService, private labelService: ILabelService - ) { - // noop - } + ) { } static readonly ID = 'functionbreakpointinput'; @@ -605,22 +645,33 @@ class FunctionBreakpointInputRenderer implements IListRenderer { - if (!template.reactedOnEvent) { - template.reactedOnEvent = true; - this.debugService.getViewModel().setSelectedBreakpoint(undefined); - if (inputBox.value && (renamed || template.breakpoint.name)) { - this.debugService.renameFunctionBreakpoint(template.breakpoint.getId(), renamed ? inputBox.value : template.breakpoint.name); + const wrapUp = (success: boolean) => { + this.view.breakpointInputFocused.set(false); + const id = template.breakpoint.getId(); + + if (success) { + if (template.type === 'name') { + this.debugService.updateFunctionBreakpoint(id, { name: inputBox.value }); + } + if (template.type === 'condition') { + this.debugService.updateFunctionBreakpoint(id, { condition: inputBox.value }); + } + if (template.type === 'hitCount') { + this.debugService.updateFunctionBreakpoint(id, { hitCondition: inputBox.value }); + } + } else { + if (template.type === 'name' && !template.breakpoint.name) { + this.debugService.removeFunctionBreakpoints(id); } else { - this.debugService.removeFunctionBreakpoints(template.breakpoint.getId()); + this.view.renderInputBox(undefined); } } }; @@ -650,7 +701,7 @@ class FunctionBreakpointInputRenderer implements IListRenderer { data.inputBox.focus(); data.inputBox.select(); @@ -672,6 +738,7 @@ class FunctionBreakpointInputRenderer implements IListRenderer { constructor( + private view: BreakpointsView, private debugService: IDebugService, private contextViewService: IContextViewService, private themeService: IThemeService @@ -693,24 +760,22 @@ class ExceptionBreakpointInputRenderer implements IListRenderer { - if (!template.reactedOnEvent) { - template.reactedOnEvent = true; - this.debugService.getViewModel().setSelectedBreakpoint(undefined); - let newCondition = template.breakpoint.condition; - if (success) { - newCondition = inputBox.value !== '' ? inputBox.value : undefined; - } - this.debugService.setExceptionBreakpointCondition(template.breakpoint, newCondition); + this.view.breakpointInputFocused.set(false); + let newCondition = template.breakpoint.condition; + if (success) { + newCondition = inputBox.value !== '' ? inputBox.value : undefined; } + this.debugService.setExceptionBreakpointCondition(template.breakpoint, newCondition); }; toDispose.push(dom.addStandardDisposableListener(inputBox.inputElement, 'keydown', (e: IKeyboardEvent) => { @@ -736,7 +801,6 @@ class ExceptionBreakpointInputRenderer implements IListRenderer { + const debugService = accessor.get(IDebugService); + if (breakpoint instanceof Breakpoint) { + await debugService.removeBreakpoints(breakpoint.getId()); + } else if (breakpoint instanceof FunctionBreakpoint) { + await debugService.removeFunctionBreakpoints(breakpoint.getId()); + } else if (breakpoint instanceof DataBreakpoint) { + await debugService.removeDataBreakpoints(breakpoint.getId()); + } + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.removeAllBreakpoints', + title: { + original: 'Remove All Breakpoints', + value: localize('removeAllBreakpoints', "Remove All Breakpoints"), + mnemonicTitle: localize({ key: 'miRemoveAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Remove &&All Breakpoints") + }, + f1: true, + icon: icons.breakpointsRemoveAll, + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 30, + when: ContextKeyEqualsExpr.create('view', BREAKPOINTS_VIEW_ID) + }, { + id: MenuId.DebugBreakpointsContext, + group: '3_modification', + order: 20, + when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINT_ITEM_TYPE.notEqualsTo('exceptionBreakpoint')) + }, { + id: MenuId.MenubarDebugMenu, + group: '5_breakpoints', + order: 3, + when: CONTEXT_DEBUGGERS_AVAILABLE + }] + }); + } + + run(accessor: ServicesAccessor): void { + const debugService = accessor.get(IDebugService); + debugService.removeBreakpoints(); + debugService.removeFunctionBreakpoints(); + debugService.removeDataBreakpoints(); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.enableAllBreakpoints', + title: { + original: '', + value: localize('enableAllBreakpoints', "Enable All Breakpoints"), + mnemonicTitle: localize({ key: 'miEnableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "&&Enable All Breakpoints"), + }, + f1: true, + precondition: CONTEXT_DEBUGGERS_AVAILABLE, + menu: [{ + id: MenuId.DebugBreakpointsContext, + group: 'z_commands', + order: 10, + when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINT_ITEM_TYPE.notEqualsTo('exceptionBreakpoint')) + }, { + id: MenuId.MenubarDebugMenu, + group: '5_breakpoints', + order: 1, + when: CONTEXT_DEBUGGERS_AVAILABLE + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + const debugService = accessor.get(IDebugService); + await debugService.enableOrDisableBreakpoints(true); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.disableAllBreakpoints', + title: { + original: 'Disable All Breakpoints', + value: localize('disableAllBreakpoints', "Disable All Breakpoints"), + mnemonicTitle: localize({ key: 'miDisableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Disable A&&ll Breakpoints") + }, + f1: true, + precondition: CONTEXT_DEBUGGERS_AVAILABLE, + menu: [{ + id: MenuId.DebugBreakpointsContext, + group: 'z_commands', + order: 20, + when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINT_ITEM_TYPE.notEqualsTo('exceptionBreakpoint')) + }, { + id: MenuId.MenubarDebugMenu, + group: '5_breakpoints', + order: 2, + + when: CONTEXT_DEBUGGERS_AVAILABLE + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + const debugService = accessor.get(IDebugService); + await debugService.enableOrDisableBreakpoints(false); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.reapplyBreakpointsAction', + title: localize('reapplyAllBreakpoints', "Reapply All Breakpoints"), + f1: true, + precondition: CONTEXT_IN_DEBUG_MODE, + menu: [{ + id: MenuId.DebugBreakpointsContext, + group: 'z_commands', + order: 30, + when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINT_ITEM_TYPE.notEqualsTo('exceptionBreakpoint')) + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + const debugService = accessor.get(IDebugService); + await debugService.setBreakpointsActivated(true); + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'debug.editBreakpoint', + viewId: BREAKPOINTS_VIEW_ID, + title: localize('editCondition', "Edit Condition..."), + icon: Codicon.edit, + precondition: CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, + menu: [{ + id: MenuId.DebugBreakpointsContext, + group: 'navigation', + order: 10 + }, { + id: MenuId.DebugBreakpointsContext, + group: 'inline', + order: 10 + }] + }); + } + + async runInView(accessor: ServicesAccessor, view: BreakpointsView, breakpoint: ExceptionBreakpoint | Breakpoint | FunctionBreakpoint): Promise { + const debugService = accessor.get(IDebugService); + const editorService = accessor.get(IEditorService); + if (breakpoint instanceof Breakpoint) { + const editor = await openBreakpointSource(breakpoint, false, false, true, debugService, editorService); + if (editor) { + const codeEditor = editor.getControl(); + if (isCodeEditor(codeEditor)) { + codeEditor.getContribution(BREAKPOINT_EDITOR_CONTRIBUTION_ID).showBreakpointWidget(breakpoint.lineNumber, breakpoint.column); + } + } + } else { + view.renderInputBox({ breakpoint, type: 'condition' }); + } + } +}); + + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'debug.editFunctionBreakpoint', + viewId: BREAKPOINTS_VIEW_ID, + title: localize('editBreakpoint', "Edit Function Breakpoint..."), + menu: [{ + id: MenuId.DebugBreakpointsContext, + group: '1_breakpoints', + order: 10, + when: CONTEXT_BREAKPOINT_ITEM_TYPE.isEqualTo('functionBreakpoint') + }] + }); + } + + runInView(_accessor: ServicesAccessor, view: BreakpointsView, breakpoint: IFunctionBreakpoint) { + view.renderInputBox({ breakpoint, type: 'name' }); + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'debug.editFunctionBreakpointHitCount', + viewId: BREAKPOINTS_VIEW_ID, + title: localize('editHitCount', "Edit Hit Count..."), + precondition: CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, + menu: [{ + id: MenuId.DebugBreakpointsContext, + group: 'navigation', + order: 20, + when: CONTEXT_BREAKPOINT_ITEM_TYPE.isEqualTo('functionBreakpoint') + }] + }); + } + + runInView(_accessor: ServicesAccessor, view: BreakpointsView, breakpoint: IFunctionBreakpoint) { + view.renderInputBox({ breakpoint, type: 'hitCount' }); + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts index 9fd5d78b..fa7901ea 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts @@ -127,17 +127,21 @@ export class CallStackEditorContribution implements IEditorContribution { const isSessionFocused = s === focusedStackFrame?.thread.session; s.getAllThreads().forEach(t => { if (t.stopped) { - let candidateStackFrame = t === focusedStackFrame?.thread ? focusedStackFrame : undefined; - if (!candidateStackFrame) { - const callStack = t.getCallStack(); - if (callStack.length) { - candidateStackFrame = callStack[0]; + const callStack = t.getCallStack(); + const stackFrames: IStackFrame[] = []; + if (callStack.length > 0) { + // Always decorate top stack frame, and decorate focused stack frame if it is not the top stack frame + if (focusedStackFrame && !focusedStackFrame.equals(callStack[0])) { + stackFrames.push(focusedStackFrame); } + stackFrames.push(callStack[0]); } - if (candidateStackFrame && this.uriIdentityService.extUri.isEqual(candidateStackFrame.source.uri, this.editor.getModel()?.uri)) { - decorations.push(...createDecorationsForStackFrame(candidateStackFrame, this.topStackFrameRange, isSessionFocused)); - } + stackFrames.forEach(candidateStackFrame => { + if (candidateStackFrame && this.uriIdentityService.extUri.isEqual(candidateStackFrame.source.uri, this.editor.getModel()?.uri)) { + decorations.push(...createDecorationsForStackFrame(candidateStackFrame, this.topStackFrameRange, isSessionFocused)); + } + }); } }); }); diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 70851479..1fc41463 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -3,22 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { RunOnceScheduler } from 'vs/base/common/async'; import * as dom from 'vs/base/browser/dom'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { IDebugService, State, IStackFrame, IDebugSession, IThread, CONTEXT_CALLSTACK_ITEM_TYPE, IDebugModel } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, State, IStackFrame, IDebugSession, IThread, CONTEXT_CALLSTACK_ITEM_TYPE, IDebugModel, CALLSTACK_VIEW_ID, CONTEXT_DEBUG_STATE, getStateLabel } from 'vs/workbench/contrib/debug/common/debug'; import { Thread, StackFrame, ThreadAndSessionIds } from 'vs/workbench/contrib/debug/common/debugModel'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { MenuId, IMenu, IMenuService, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { MenuId, IMenu, IMenuService, MenuItemAction, SubmenuItemAction, registerAction2 } from 'vs/platform/actions/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { IAction, Action } from 'vs/base/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IContextKey, IContextKeyService, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ViewPane, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { ILabelService } from 'vs/platform/label/common/label'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { createAndFillInContextMenuActions, createAndFillInActionBarActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; @@ -33,7 +32,6 @@ import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; import { STOP_ID, STOP_LABEL, DISCONNECT_ID, DISCONNECT_LABEL, RESTART_SESSION_ID, RESTART_LABEL, STEP_OVER_ID, STEP_OVER_LABEL, STEP_INTO_LABEL, STEP_INTO_ID, STEP_OUT_LABEL, STEP_OUT_ID, PAUSE_ID, PAUSE_LABEL, CONTINUE_ID, CONTINUE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { CollapseAction } from 'vs/workbench/browser/viewlet'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; @@ -47,6 +45,8 @@ import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree' import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { localize } from 'vs/nls'; +import { Codicon } from 'vs/base/common/codicons'; const $ = dom.$; @@ -162,7 +162,7 @@ export class CallStackView extends ViewPane { this.stateMessageLabel.classList.toggle('exception', thread.stoppedDetails.reason === 'exception'); this.stateMessage.hidden = false; } else if (sessions.length === 1 && sessions[0].state === State.Running) { - this.stateMessageLabel.textContent = nls.localize({ key: 'running', comment: ['indicates state'] }, "Running"); + this.stateMessageLabel.textContent = localize({ key: 'running', comment: ['indicates state'] }, "Running"); this.stateMessageLabel.title = sessions[0].getLabel(); this.stateMessageLabel.classList.remove('exception'); this.stateMessage.hidden = false; @@ -205,14 +205,6 @@ export class CallStackView extends ViewPane { this.stateMessageLabel = dom.append(this.stateMessage, $('span.label')); } - getActions(): IAction[] { - if (this.stateMessage.hidden) { - return [new CollapseAction(() => this.tree, true, 'explorer-action ' + ThemeIcon.asClassName(icons.debugCollapseAll))]; - } - - return []; - } - renderBody(container: HTMLElement): void { super.renderBody(container); this.element.classList.add('debug-pane'); @@ -220,11 +212,11 @@ export class CallStackView extends ViewPane { const treeContainer = renderViewTree(container); this.dataSource = new CallStackDataSource(this.debugService); - const sessionsRenderer = this.instantiationService.createInstance(SessionsRenderer, this.menu); + const sessionsRenderer = this.instantiationService.createInstance(SessionsRenderer, this.menu, this.callStackItemType); this.tree = >this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree, 'CallStackView', treeContainer, new CallStackDelegate(), new CallStackCompressionDelegate(this.debugService), [ sessionsRenderer, - new ThreadsRenderer(this.instantiationService), - this.instantiationService.createInstance(StackFramesRenderer), + new ThreadsRenderer(this.callStackItemType, this.instantiationService), + this.instantiationService.createInstance(StackFramesRenderer, this.callStackItemType), new ErrorsRenderer(), new LoadAllRenderer(this.themeService), new ShowMoreRenderer(this.themeService) @@ -259,7 +251,7 @@ export class CallStackView extends ViewPane { return LoadAllRenderer.LABEL; } - return nls.localize('showMoreStackFrames2', "Show More Stack Frames"); + return localize('showMoreStackFrames2', "Show More Stack Frames"); }, getCompressedNodeKeyboardNavigationLabel: (e: CallStackItem[]) => { const firstItem = e[0]; @@ -378,6 +370,10 @@ export class CallStackView extends ViewPane { this.tree.domFocus(); } + collapseAll(): void { + this.tree.collapseAll(); + } + private async updateTreeSelection(): Promise { if (!this.tree || !this.tree.getInput()) { // Tree not initialized yet @@ -493,6 +489,7 @@ class SessionsRenderer implements ICompressibleTreeRenderer, @IInstantiationService private readonly instantiationService: IInstantiationService ) { } @@ -532,7 +529,7 @@ class SessionsRenderer implements ICompressibleTreeRenderer t.stopped); @@ -542,6 +539,7 @@ class SessionsRenderer implements ICompressibleTreeRenderer /^inline/.test(g))); data.actionBar.clear(); @@ -557,7 +555,7 @@ class SessionsRenderer implements ICompressibleTreeRenderer { static readonly ID = 'thread'; - constructor(private readonly instantiationService: IInstantiationService) { } + constructor( + private callStackItemType: IContextKey, + private readonly instantiationService: IInstantiationService + ) { } get templateId(): string { return ThreadsRenderer.ID; @@ -591,11 +592,12 @@ class ThreadsRenderer implements ICompressibleTreeRenderer, index: number, data: IThreadTemplateData): void { const thread = element.element; - data.thread.title = nls.localize('thread', "Thread"); + data.thread.title = localize('thread', "Thread"); data.label.set(thread.name, createMatches(element.filterData)); data.stateLabel.textContent = thread.stateLabel; data.actionBar.clear(); + this.callStackItemType.set('thread'); const actions = getActions(this.instantiationService, thread); data.actionBar.push(actions, { icon: true, label: false }); } @@ -613,8 +615,9 @@ class StackFramesRenderer implements ICompressibleTreeRenderer, @ILabelService private readonly labelService: ILabelService, - @INotificationService private readonly notificationService: INotificationService + @INotificationService private readonly notificationService: INotificationService, ) { } get templateId(): string { @@ -659,8 +662,9 @@ class StackFramesRenderer implements ICompressibleTreeRenderer { + const action = new Action('debug.callStack.restartFrame', localize('restartFrame', "Restart Frame"), ThemeIcon.asClassName(icons.debugRestartFrame), true, async () => { try { await stackFrame.restart(); } catch (e) { @@ -710,7 +714,7 @@ class ErrorsRenderer implements ICompressibleTreeRenderer { static readonly ID = 'loadAll'; - static readonly LABEL = nls.localize('loadAllStackFrames', "Load All Stack Frames"); + static readonly LABEL = localize('loadAllStackFrames', "Load All Stack Frames"); constructor(private readonly themeService: IThemeService) { } @@ -766,9 +770,9 @@ class ShowMoreRenderer implements ICompressibleTreeRenderer, index: number, data: ILabelTemplateData): void { const stackFrames = element.element; if (stackFrames.every(sf => !!(sf.source && sf.source.origin && sf.source.origin === stackFrames[0].source.origin))) { - data.label.textContent = nls.localize('showMoreAndOrigin', "Show {0} More: {1}", stackFrames.length, stackFrames[0].source.origin); + data.label.textContent = localize('showMoreAndOrigin', "Show {0} More: {1}", stackFrames.length, stackFrames[0].source.origin); } else { - data.label.textContent = nls.localize('showMoreStackFrames', "Show {0} More Stack Frames", stackFrames.length); + data.label.textContent = localize('showMoreStackFrames', "Show {0} More Stack Frames", stackFrames.length); } } @@ -930,26 +934,26 @@ class CallStackDataSource implements IAsyncDataSource { getWidgetAriaLabel(): string { - return nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'callStackAriaLabel' }, "Debug Call Stack"); + return localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'callStackAriaLabel' }, "Debug Call Stack"); } getAriaLabel(element: CallStackItem): string { if (element instanceof Thread) { - return nls.localize({ key: 'threadAriaLabel', comment: ['Placeholders stand for the thread name and the thread state.For example "Thread 1" and "Stopped'] }, "Thread {0} {1}", element.name, element.stateLabel); + return localize({ key: 'threadAriaLabel', comment: ['Placeholders stand for the thread name and the thread state.For example "Thread 1" and "Stopped'] }, "Thread {0} {1}", element.name, element.stateLabel); } if (element instanceof StackFrame) { - return nls.localize('stackFrameAriaLabel', "Stack Frame {0}, line {1}, {2}", element.name, element.range.startLineNumber, getSpecificSourceName(element)); + return localize('stackFrameAriaLabel', "Stack Frame {0}, line {1}, {2}", element.name, element.range.startLineNumber, getSpecificSourceName(element)); } if (isDebugSession(element)) { const thread = element.getAllThreads().find(t => t.stopped); - const state = thread ? thread.stateLabel : nls.localize({ key: 'running', comment: ['indicates state'] }, "Running"); - return nls.localize({ key: 'sessionLabel', comment: ['Placeholders stand for the session name and the session state. For example "Launch Program" and "Running"'] }, "Session {0} {1}", element.getLabel(), state); + const state = thread ? thread.stateLabel : localize({ key: 'running', comment: ['indicates state'] }, "Running"); + return localize({ key: 'sessionLabel', comment: ['Placeholders stand for the session name and the session state. For example "Launch Program" and "Running"'] }, "Session {0} {1}", element.getLabel(), state); } if (typeof element === 'string') { return element; } if (element instanceof Array) { - return nls.localize('showMoreStackFrames', "Show {0} More Stack Frames", element.length); + return localize('showMoreStackFrames', "Show {0} More Stack Frames", element.length); } // element instanceof ThreadAndSessionIds @@ -1121,3 +1125,26 @@ class CallStackCompressionDelegate implements ITreeCompressionDelegate { + constructor() { + super({ + id: 'callStack.collapse', + viewId: CALLSTACK_VIEW_ID, + title: localize('collapse', "Collapse All"), + f1: false, + icon: Codicon.collapseAll, + precondition: CONTEXT_DEBUG_STATE.isEqualTo(getStateLabel(State.Stopped)), + menu: { + id: MenuId.ViewTitle, + order: 10, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', CALLSTACK_VIEW_ID) + } + }); + } + + runInView(_accessor: ServicesAccessor, view: CallStackView) { + view.collapseAll(); + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 9438ac11..a2f0090e 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -17,12 +17,11 @@ import { CallStackView } from 'vs/workbench/contrib/debug/browser/callStackView' import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IDebugService, VIEWLET_ID, DEBUG_PANEL_ID, CONTEXT_IN_DEBUG_MODE, INTERNAL_CONSOLE_OPTIONS_SCHEMA, - CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, + CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, getStateLabel, State, CONTEXT_WATCH_ITEM_TYPE, } from 'vs/workbench/contrib/debug/common/debug'; -import { StartAction, AddFunctionBreakpointAction, ConfigureAction, DisableAllBreakpointsAction, EnableAllBreakpointsAction, RemoveAllBreakpointsAction, RunAction, ReapplyBreakpointsAction, SelectAndStartAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { DebugToolBar } from 'vs/workbench/contrib/debug/browser/debugToolBar'; import { DebugService } from 'vs/workbench/contrib/debug/browser/debugService'; -import { registerCommands, ADD_CONFIGURATION_ID, TOGGLE_INLINE_BREAKPOINT_ID, COPY_STACK_TRACE_ID, REVERSE_CONTINUE_ID, STEP_BACK_ID, RESTART_SESSION_ID, TERMINATE_THREAD_ID, STEP_OVER_ID, STEP_INTO_ID, STEP_OUT_ID, PAUSE_ID, DISCONNECT_ID, STOP_ID, RESTART_FRAME_ID, CONTINUE_ID, FOCUS_REPL_ID, JUMP_TO_CURSOR_ID, RESTART_LABEL, STEP_INTO_LABEL, STEP_OVER_LABEL, STEP_OUT_LABEL, PAUSE_LABEL, DISCONNECT_LABEL, STOP_LABEL, CONTINUE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; +import { registerCommands, ADD_CONFIGURATION_ID, TOGGLE_INLINE_BREAKPOINT_ID, COPY_STACK_TRACE_ID, RESTART_SESSION_ID, TERMINATE_THREAD_ID, STEP_OVER_ID, STEP_INTO_ID, STEP_OUT_ID, PAUSE_ID, DISCONNECT_ID, STOP_ID, RESTART_FRAME_ID, CONTINUE_ID, FOCUS_REPL_ID, JUMP_TO_CURSOR_ID, RESTART_LABEL, STEP_INTO_LABEL, STEP_OVER_LABEL, STEP_OUT_LABEL, PAUSE_LABEL, DISCONNECT_LABEL, STOP_LABEL, CONTINUE_LABEL, DEBUG_START_LABEL, DEBUG_START_COMMAND_ID, DEBUG_RUN_LABEL, DEBUG_RUN_COMMAND_ID, EDIT_EXPRESSION_COMMAND_ID, REMOVE_EXPRESSION_COMMAND_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { StatusBarColorProvider } from 'vs/workbench/contrib/debug/browser/statusbarColorProvider'; import { IViewsRegistry, Extensions as ViewExtensions, IViewContainersRegistry, ViewContainerLocation, ViewContainer } from 'vs/workbench/common/views'; import { isMacintosh, isWeb } from 'vs/base/common/platform'; @@ -32,14 +31,13 @@ import { DebugStatusContribution } from 'vs/workbench/contrib/debug/browser/debu import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { LoadedScriptsView } from 'vs/workbench/contrib/debug/browser/loadedScriptsView'; -import { ADD_LOG_POINT_ID, TOGGLE_CONDITIONAL_BREAKPOINT_ID, TOGGLE_BREAKPOINT_ID, RunToCursorAction, registerEditorActions } from 'vs/workbench/contrib/debug/browser/debugEditorActions'; -import { WatchExpressionsView } from 'vs/workbench/contrib/debug/browser/watchExpressionsView'; +import { RunToCursorAction, registerEditorActions } from 'vs/workbench/contrib/debug/browser/debugEditorActions'; +import { WatchExpressionsView, ADD_WATCH_LABEL, REMOVE_WATCH_EXPRESSIONS_COMMAND_ID, REMOVE_WATCH_EXPRESSIONS_LABEL, ADD_WATCH_ID } from 'vs/workbench/contrib/debug/browser/watchExpressionsView'; import { VariablesView, SET_VARIABLE_ID, COPY_VALUE_ID, BREAK_WHEN_VALUE_CHANGES_ID, COPY_EVALUATE_PATH_ID, ADD_TO_WATCH_ID } from 'vs/workbench/contrib/debug/browser/variablesView'; -import { ClearReplAction, Repl } from 'vs/workbench/contrib/debug/browser/repl'; +import { Repl } from 'vs/workbench/contrib/debug/browser/repl'; import { DebugContentProvider } from 'vs/workbench/contrib/debug/common/debugContentProvider'; import { WelcomeView } from 'vs/workbench/contrib/debug/browser/welcomeView'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { DebugViewPaneContainer, OpenDebugConsoleAction, OpenDebugViewletAction } from 'vs/workbench/contrib/debug/browser/debugViewlet'; +import { DebugViewPaneContainer, OpenDebugViewletAction, OPEN_REPL_COMMAND_ID } from 'vs/workbench/contrib/debug/browser/debugViewlet'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { CallStackEditorContribution } from 'vs/workbench/contrib/debug/browser/callStackEditorContribution'; import { BreakpointEditorContribution } from 'vs/workbench/contrib/debug/browser/breakpointEditorContribution'; @@ -56,7 +54,6 @@ import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; const registry = Registry.as(WorkbenchActionRegistryExtensions.WorkbenchActions); const debugCategory = nls.localize('debugCategory', "Debug"); -const runCategroy = nls.localize('runCategory', "Run"); registerWorkbenchContributions(); registerColors(); registerCommandsAndActions(); @@ -64,8 +61,6 @@ registerDebugMenu(); registerEditorActions(); registerCommands(); registerDebugPanel(); -registry.registerWorkbenchAction(SyncActionDescriptor.from(StartAction, { primary: KeyCode.F5 }, CONTEXT_IN_DEBUG_MODE.toNegated()), 'Debug: Start Debugging', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); -registry.registerWorkbenchAction(SyncActionDescriptor.from(RunAction, { primary: KeyMod.CtrlCmd | KeyCode.F5, mac: { primary: KeyMod.WinCtrl | KeyCode.F5 } }), 'Run: Start Without Debugging', runCategroy, CONTEXT_DEBUGGERS_AVAILABLE); registerSingleton(IDebugService, DebugService, true); registerDebugView(); @@ -102,18 +97,10 @@ function regsiterEditorContributions(): void { function registerCommandsAndActions(): void { - registry.registerWorkbenchAction(SyncActionDescriptor.from(ConfigureAction), 'Debug: Open launch.json', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - registry.registerWorkbenchAction(SyncActionDescriptor.from(AddFunctionBreakpointAction), 'Debug: Add Function Breakpoint', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - registry.registerWorkbenchAction(SyncActionDescriptor.from(ReapplyBreakpointsAction), 'Debug: Reapply All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - registry.registerWorkbenchAction(SyncActionDescriptor.from(RemoveAllBreakpointsAction), 'Debug: Remove All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - registry.registerWorkbenchAction(SyncActionDescriptor.from(EnableAllBreakpointsAction), 'Debug: Enable All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - registry.registerWorkbenchAction(SyncActionDescriptor.from(DisableAllBreakpointsAction), 'Debug: Disable All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - registry.registerWorkbenchAction(SyncActionDescriptor.from(SelectAndStartAction), 'Debug: Select and Start Debugging', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - registry.registerWorkbenchAction(SyncActionDescriptor.from(ClearReplAction), 'Debug: Clear Console', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - const registerDebugCommandPaletteItem = (id: string, title: string, when?: ContextKeyExpression, precondition?: ContextKeyExpression) => { MenuRegistry.appendMenuItem(MenuId.CommandPalette, { when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, when), + group: debugCategory, command: { id, title: `Debug: ${title}`, @@ -136,33 +123,8 @@ function registerCommandsAndActions(): void { registerDebugCommandPaletteItem(JUMP_TO_CURSOR_ID, nls.localize('SetNextStatement', "Set Next Statement"), CONTEXT_JUMP_TO_CURSOR_SUPPORTED); registerDebugCommandPaletteItem(RunToCursorAction.ID, RunToCursorAction.LABEL, ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped'))); registerDebugCommandPaletteItem(TOGGLE_INLINE_BREAKPOINT_ID, nls.localize('inlineBreakpoint', "Inline Breakpoint")); - - // Debug toolbar - - const registerDebugToolBarItem = (id: string, title: string, order: number, icon: { light?: URI, dark?: URI } | ThemeIcon, when?: ContextKeyExpression, precondition?: ContextKeyExpression) => { - MenuRegistry.appendMenuItem(MenuId.DebugToolBar, { - group: 'navigation', - when, - order, - command: { - id, - title, - icon, - precondition - } - }); - }; - - registerDebugToolBarItem(CONTINUE_ID, CONTINUE_LABEL, 10, icons.debugContinue, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); - registerDebugToolBarItem(PAUSE_ID, PAUSE_LABEL, 10, icons.debugPause, CONTEXT_DEBUG_STATE.notEqualsTo('stopped'), CONTEXT_DEBUG_STATE.isEqualTo('running')); - registerDebugToolBarItem(STOP_ID, STOP_LABEL, 70, icons.debugStop, CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated()); - registerDebugToolBarItem(DISCONNECT_ID, DISCONNECT_LABEL, 70, icons.debugDisconnect, CONTEXT_FOCUSED_SESSION_IS_ATTACH); - registerDebugToolBarItem(STEP_OVER_ID, STEP_OVER_LABEL, 20, icons.debugStepOver, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); - registerDebugToolBarItem(STEP_INTO_ID, STEP_INTO_LABEL, 30, icons.debugStepInto, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); - registerDebugToolBarItem(STEP_OUT_ID, STEP_OUT_LABEL, 40, icons.debugStepOut, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); - registerDebugToolBarItem(RESTART_SESSION_ID, RESTART_LABEL, 60, icons.debugRestart); - registerDebugToolBarItem(STEP_BACK_ID, nls.localize('stepBackDebug', "Step Back"), 50, icons.debugStepBack, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); - registerDebugToolBarItem(REVERSE_CONTINUE_ID, nls.localize('reverseContinue', "Reverse"), 60, icons.debugReverseContinue, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); + registerDebugCommandPaletteItem(DEBUG_START_COMMAND_ID, DEBUG_START_LABEL, ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing)))); + registerDebugCommandPaletteItem(DEBUG_RUN_COMMAND_ID, DEBUG_RUN_LABEL, ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing)))); // Debug callstack context menu const registerDebugViewMenuItem = (menuId: MenuId, id: string, title: string, order: number, when?: ContextKeyExpression, precondition?: ContextKeyExpression, group = 'navigation') => { @@ -194,6 +156,12 @@ function registerCommandsAndActions(): void { registerDebugViewMenuItem(MenuId.DebugVariablesContext, ADD_TO_WATCH_ID, nls.localize('addToWatchExpressions', "Add to Watch"), 100, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, undefined, 'z_commands'); registerDebugViewMenuItem(MenuId.DebugVariablesContext, BREAK_WHEN_VALUE_CHANGES_ID, nls.localize('breakWhenValueChanges', "Break When Value Changes"), 200, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, undefined, 'z_commands'); + registerDebugViewMenuItem(MenuId.DebugWatchContext, ADD_WATCH_ID, ADD_WATCH_LABEL, 10, undefined, undefined, '3_modification'); + registerDebugViewMenuItem(MenuId.DebugWatchContext, EDIT_EXPRESSION_COMMAND_ID, nls.localize('editWatchExpression', "Edit Expression"), 20, CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), undefined, '3_modification'); + registerDebugViewMenuItem(MenuId.DebugWatchContext, COPY_VALUE_ID, nls.localize('copyValue', "Copy Value"), 30, ContextKeyExpr.or(CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), CONTEXT_WATCH_ITEM_TYPE.isEqualTo('variable')), CONTEXT_IN_DEBUG_MODE, '3_modification'); + registerDebugViewMenuItem(MenuId.DebugWatchContext, REMOVE_EXPRESSION_COMMAND_ID, nls.localize('removeWatchExpression', "Remove Expression"), 10, CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), undefined, 'z_commands'); + registerDebugViewMenuItem(MenuId.DebugWatchContext, REMOVE_WATCH_EXPRESSIONS_COMMAND_ID, REMOVE_WATCH_EXPRESSIONS_LABEL, 20, undefined, undefined, 'z_commands'); + // Touch Bar if (isMacintosh) { @@ -210,8 +178,8 @@ function registerCommandsAndActions(): void { }); }; - registerTouchBarEntry(StartAction.ID, StartAction.LABEL, 0, CONTEXT_IN_DEBUG_MODE.toNegated(), FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/continue-tb.png', require)); - registerTouchBarEntry(RunAction.ID, RunAction.LABEL, 1, CONTEXT_IN_DEBUG_MODE.toNegated(), FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/continue-without-debugging-tb.png', require)); + registerTouchBarEntry(DEBUG_START_COMMAND_ID, DEBUG_START_LABEL, 0, CONTEXT_IN_DEBUG_MODE.toNegated(), FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/continue-tb.png', require)); + registerTouchBarEntry(DEBUG_RUN_COMMAND_ID, DEBUG_RUN_LABEL, 1, CONTEXT_IN_DEBUG_MODE.toNegated(), FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/continue-without-debugging-tb.png', require)); registerTouchBarEntry(CONTINUE_ID, CONTINUE_LABEL, 0, CONTEXT_DEBUG_STATE.isEqualTo('stopped'), FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/continue-tb.png', require)); registerTouchBarEntry(PAUSE_ID, PAUSE_LABEL, 1, ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, ContextKeyExpr.notEquals('debugState', 'stopped')), FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/pause-tb.png', require)); registerTouchBarEntry(STEP_OVER_ID, STEP_OVER_LABEL, 2, CONTEXT_IN_DEBUG_MODE, FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/stepover-tb.png', require)); @@ -234,21 +202,12 @@ function registerDebugMenu(): void { order: 4 }); - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '4_panels', - command: { - id: OpenDebugConsoleAction.ID, - title: nls.localize({ key: 'miToggleDebugConsole', comment: ['&& denotes a mnemonic'] }, "De&&bug Console") - }, - order: 2 - }); - // Debug menu MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { group: '1_debug', command: { - id: StartAction.ID, + id: DEBUG_START_COMMAND_ID, title: nls.localize({ key: 'miStartDebugging', comment: ['&& denotes a mnemonic'] }, "&&Start Debugging") }, order: 1, @@ -258,7 +217,7 @@ function registerDebugMenu(): void { MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { group: '1_debug', command: { - id: RunAction.ID, + id: DEBUG_RUN_COMMAND_ID, title: nls.localize({ key: 'miRun', comment: ['&& denotes a mnemonic'] }, "Run &&Without Debugging") }, order: 2, @@ -288,15 +247,6 @@ function registerDebugMenu(): void { }); // Configuration - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '2_configuration', - command: { - id: ConfigureAction.ID, - title: nls.localize({ key: 'miOpenConfigurations', comment: ['&& denotes a mnemonic'] }, "Open &&Configurations") - }, - order: 1, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { group: '2_configuration', @@ -354,25 +304,6 @@ function registerDebugMenu(): void { }); // New Breakpoints - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '4_new_breakpoint', - command: { - id: TOGGLE_BREAKPOINT_ID, - title: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breakpoint") - }, - order: 1, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { - group: '1_breakpoints', - command: { - id: TOGGLE_CONDITIONAL_BREAKPOINT_ID, - title: nls.localize({ key: 'miConditionalBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Conditional Breakpoint...") - }, - order: 1, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { group: '1_breakpoints', @@ -384,26 +315,6 @@ function registerDebugMenu(): void { when: CONTEXT_DEBUGGERS_AVAILABLE }); - MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { - group: '1_breakpoints', - command: { - id: AddFunctionBreakpointAction.ID, - title: nls.localize({ key: 'miFunctionBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Function Breakpoint...") - }, - order: 3, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { - group: '1_breakpoints', - command: { - id: ADD_LOG_POINT_ID, - title: nls.localize({ key: 'miLogPoint', comment: ['&& denotes a mnemonic'] }, "&&Logpoint...") - }, - order: 4, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { group: '4_new_breakpoint', title: nls.localize({ key: 'miNewBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&New Breakpoint"), @@ -412,36 +323,7 @@ function registerDebugMenu(): void { when: CONTEXT_DEBUGGERS_AVAILABLE }); - // Modify Breakpoints - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '5_breakpoints', - command: { - id: EnableAllBreakpointsAction.ID, - title: nls.localize({ key: 'miEnableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "&&Enable All Breakpoints") - }, - order: 1, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '5_breakpoints', - command: { - id: DisableAllBreakpointsAction.ID, - title: nls.localize({ key: 'miDisableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Disable A&&ll Breakpoints") - }, - order: 2, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '5_breakpoints', - command: { - id: RemoveAllBreakpointsAction.ID, - title: nls.localize({ key: 'miRemoveAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Remove &&All Breakpoints") - }, - order: 3, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); + // Breakpoint actions are registered from breakpointsView.ts // Install Debuggers MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { @@ -463,7 +345,7 @@ function registerDebugPanel(): void { icon: icons.debugConsoleViewIcon, ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [DEBUG_PANEL_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), storageId: DEBUG_PANEL_ID, - focusCommand: { id: OpenDebugConsoleAction.ID }, + focusCommand: { id: OPEN_REPL_COMMAND_ID }, order: 2, hideIfEmpty: true }, ViewContainerLocation.Panel); @@ -477,8 +359,6 @@ function registerDebugPanel(): void { when: CONTEXT_DEBUGGERS_AVAILABLE, ctorDescriptor: new SyncDescriptor(Repl), }], VIEW_CONTAINER); - - registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenDebugConsoleAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Y }), 'View: Debug Console', CATEGORIES.View.value, CONTEXT_DEBUGGERS_AVAILABLE); } diff --git a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts index 3ab90797..b600fc99 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts @@ -11,7 +11,7 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { SelectBox, ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IDebugService, IDebugSession, IDebugConfiguration, IConfig, ILaunch } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IDebugSession, IDebugConfiguration, IConfig, ILaunch, State } from 'vs/workbench/contrib/debug/common/debug'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { attachSelectBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { selectBorder, selectBackground } from 'vs/platform/theme/common/colorRegistry'; @@ -76,7 +76,9 @@ export class StartDebugActionViewItem implements IActionViewItem { this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.CLICK, () => { this.start.blur(); - this.actionRunner.run(this.action, this.context); + if (this.debugService.state !== State.Initializing) { + this.actionRunner.run(this.action, this.context); + } })); this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.MOUSE_DOWN, (e: MouseEvent) => { @@ -93,7 +95,7 @@ export class StartDebugActionViewItem implements IActionViewItem { this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { const event = new StandardKeyboardEvent(e); - if (event.equals(KeyCode.Enter)) { + if (event.equals(KeyCode.Enter) && this.debugService.state !== State.Initializing) { this.actionRunner.run(this.action, this.context); } if (event.equals(KeyCode.RightArrow)) { diff --git a/src/vs/workbench/contrib/debug/browser/debugActions.ts b/src/vs/workbench/contrib/debug/browser/debugActions.ts deleted file mode 100644 index 3c947210..00000000 --- a/src/vs/workbench/contrib/debug/browser/debugActions.ts +++ /dev/null @@ -1,421 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as nls from 'vs/nls'; -import { Action } from 'vs/base/common/actions'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IDebugService, State, IEnablement, IBreakpoint, IDebugSession, ILaunch } from 'vs/workbench/contrib/debug/common/debug'; -import { Variable, Breakpoint, FunctionBreakpoint, Expression } from 'vs/workbench/contrib/debug/common/debugModel'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { deepClone } from 'vs/base/common/objects'; -import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; - -export abstract class AbstractDebugAction extends Action { - - constructor( - id: string, label: string, cssClass: string, - @IDebugService protected debugService: IDebugService, - @IKeybindingService protected keybindingService: IKeybindingService, - ) { - super(id, label, cssClass, false); - this._register(this.debugService.onDidChangeState(state => this.updateEnablement(state))); - - this.updateLabel(label); - this.updateEnablement(); - } - - run(_: any): Promise { - throw new Error('implement me'); - } - - get tooltip(): string { - const keybinding = this.keybindingService.lookupKeybinding(this.id); - const keybindingLabel = keybinding && keybinding.getLabel(); - - return keybindingLabel ? `${this.label} (${keybindingLabel})` : this.label; - } - - protected updateLabel(newLabel: string): void { - this.label = newLabel; - } - - protected updateEnablement(state = this.debugService.state): void { - this.enabled = this.isEnabled(state); - } - - protected isEnabled(_: State): boolean { - return true; - } -} - -export class ConfigureAction extends AbstractDebugAction { - static readonly ID = 'workbench.action.debug.configure'; - static readonly LABEL = nls.localize('openLaunchJson', "Open {0}", 'launch.json'); - - constructor(id: string, label: string, - @IDebugService debugService: IDebugService, - @IKeybindingService keybindingService: IKeybindingService, - @INotificationService private readonly notificationService: INotificationService, - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IQuickInputService private readonly quickInputService: IQuickInputService - ) { - super(id, label, 'debug-action ' + ThemeIcon.asClassName(icons.debugConfigure), debugService, keybindingService); - this._register(debugService.getConfigurationManager().onDidSelectConfiguration(() => this.updateClass())); - this.updateClass(); - } - - get tooltip(): string { - if (this.debugService.getConfigurationManager().selectedConfiguration.name) { - return ConfigureAction.LABEL; - } - - return nls.localize('launchJsonNeedsConfigurtion', "Configure or Fix 'launch.json'"); - } - - private updateClass(): void { - const configurationManager = this.debugService.getConfigurationManager(); - this.class = configurationManager.selectedConfiguration.name ? 'debug-action' + ThemeIcon.asClassName(icons.debugConfigure) : 'debug-action ' + ThemeIcon.asClassName(icons.debugConfigure) + ' notification'; - } - - async run(): Promise { - if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY || this.contextService.getWorkspace().folders.length === 0) { - this.notificationService.info(nls.localize('noFolderDebugConfig', "Please first open a folder in order to do advanced debug configuration.")); - return; - } - - const configurationManager = this.debugService.getConfigurationManager(); - let launch: ILaunch | undefined; - if (configurationManager.selectedConfiguration.name) { - launch = configurationManager.selectedConfiguration.launch; - } else { - const launches = configurationManager.getLaunches().filter(l => !l.hidden); - if (launches.length === 1) { - launch = launches[0]; - } else { - const picks = launches.map(l => ({ label: l.name, launch: l })); - const picked = await this.quickInputService.pick<{ label: string, launch: ILaunch }>(picks, { - activeItem: picks[0], - placeHolder: nls.localize({ key: 'selectWorkspaceFolder', comment: ['User picks a workspace folder or a workspace configuration file here. Workspace configuration files can contain settings and thus a launch.json configuration can be written into one.'] }, "Select a workspace folder to create a launch.json file in or add it to the workspace config file") - }); - if (picked) { - launch = picked.launch; - } - } - } - - if (launch) { - return launch.openConfigFile(false); - } - } -} - -export class StartAction extends AbstractDebugAction { - static ID = 'workbench.action.debug.start'; - static LABEL = nls.localize('startDebug', "Start Debugging"); - - constructor(id: string, label: string, - @IDebugService debugService: IDebugService, - @IKeybindingService keybindingService: IKeybindingService, - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - ) { - super(id, label, 'debug-action start', debugService, keybindingService); - - this._register(this.debugService.getConfigurationManager().onDidSelectConfiguration(() => this.updateEnablement())); - this._register(this.debugService.onDidNewSession(() => this.updateEnablement())); - this._register(this.debugService.onDidEndSession(() => this.updateEnablement())); - this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateEnablement())); - } - - async run(): Promise { - let { launch, name, getConfig } = this.debugService.getConfigurationManager().selectedConfiguration; - const config = await getConfig(); - const clonedConfig = deepClone(config); - return this.debugService.startDebugging(launch, clonedConfig || name, { noDebug: this.isNoDebug() }); - } - - protected isNoDebug(): boolean { - return false; - } - - static isEnabled(debugService: IDebugService) { - const sessions = debugService.getModel().getSessions(); - - if (debugService.state === State.Initializing) { - return false; - } - let { name, launch } = debugService.getConfigurationManager().selectedConfiguration; - let nameToStart = name; - - if (sessions.some(s => s.configuration.name === nameToStart && s.root === launch?.workspace)) { - // There is already a debug session running and we do not have any launch configuration selected - return false; - } - - return true; - } - - // Disabled if the launch drop down shows the launch config that is already running. - protected isEnabled(): boolean { - return StartAction.isEnabled(this.debugService); - } -} - -export class RunAction extends StartAction { - static readonly ID = 'workbench.action.debug.run'; - static LABEL = nls.localize('startWithoutDebugging', "Start Without Debugging"); - - protected isNoDebug(): boolean { - return true; - } -} - -export class SelectAndStartAction extends AbstractDebugAction { - static readonly ID = 'workbench.action.debug.selectandstart'; - static readonly LABEL = nls.localize('selectAndStartDebugging', "Select and Start Debugging"); - - constructor(id: string, label: string, - @IDebugService debugService: IDebugService, - @IKeybindingService keybindingService: IKeybindingService, - @IQuickInputService private readonly quickInputService: IQuickInputService - ) { - super(id, label, '', debugService, keybindingService); - } - - async run(): Promise { - this.quickInputService.quickAccess.show('debug '); - } -} - -export class RemoveBreakpointAction extends Action { - static readonly ID = 'workbench.debug.viewlet.action.removeBreakpoint'; - static readonly LABEL = nls.localize('removeBreakpoint', "Remove Breakpoint"); - - constructor(id: string, label: string, @IDebugService private readonly debugService: IDebugService) { - super(id, label, 'debug-action remove'); - } - - run(breakpoint: IBreakpoint): Promise { - return breakpoint instanceof Breakpoint ? this.debugService.removeBreakpoints(breakpoint.getId()) - : breakpoint instanceof FunctionBreakpoint ? this.debugService.removeFunctionBreakpoints(breakpoint.getId()) : this.debugService.removeDataBreakpoints(breakpoint.getId()); - } -} - -export class RemoveAllBreakpointsAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.removeAllBreakpoints'; - static readonly LABEL = nls.localize('removeAllBreakpoints', "Remove All Breakpoints"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, 'debug-action ' + ThemeIcon.asClassName(icons.breakpointsRemoveAll), debugService, keybindingService); - this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement())); - } - - run(): Promise { - return Promise.all([this.debugService.removeBreakpoints(), this.debugService.removeFunctionBreakpoints(), this.debugService.removeDataBreakpoints()]); - } - - protected isEnabled(_: State): boolean { - const model = this.debugService.getModel(); - return (model.getBreakpoints().length > 0 || model.getFunctionBreakpoints().length > 0 || model.getDataBreakpoints().length > 0); - } -} - -export class EnableAllBreakpointsAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.enableAllBreakpoints'; - static readonly LABEL = nls.localize('enableAllBreakpoints', "Enable All Breakpoints"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, 'debug-action enable-all-breakpoints', debugService, keybindingService); - this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement())); - } - - run(): Promise { - return this.debugService.enableOrDisableBreakpoints(true); - } - - protected isEnabled(_: State): boolean { - const model = this.debugService.getModel(); - return (>model.getBreakpoints()).concat(model.getFunctionBreakpoints()).concat(model.getExceptionBreakpoints()).some(bp => !bp.enabled); - } -} - -export class DisableAllBreakpointsAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.disableAllBreakpoints'; - static readonly LABEL = nls.localize('disableAllBreakpoints', "Disable All Breakpoints"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, 'debug-action disable-all-breakpoints', debugService, keybindingService); - this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement())); - } - - run(): Promise { - return this.debugService.enableOrDisableBreakpoints(false); - } - - protected isEnabled(_: State): boolean { - const model = this.debugService.getModel(); - return (>model.getBreakpoints()).concat(model.getFunctionBreakpoints()).concat(model.getExceptionBreakpoints()).some(bp => bp.enabled); - } -} - -export class ToggleBreakpointsActivatedAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.toggleBreakpointsActivatedAction'; - static readonly ACTIVATE_LABEL = nls.localize('activateBreakpoints', "Activate Breakpoints"); - static readonly DEACTIVATE_LABEL = nls.localize('deactivateBreakpoints', "Deactivate Breakpoints"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, 'debug-action ' + ThemeIcon.asClassName(icons.breakpointsActivate), debugService, keybindingService); - this.updateLabel(this.debugService.getModel().areBreakpointsActivated() ? ToggleBreakpointsActivatedAction.DEACTIVATE_LABEL : ToggleBreakpointsActivatedAction.ACTIVATE_LABEL); - - this._register(this.debugService.getModel().onDidChangeBreakpoints(() => { - this.updateLabel(this.debugService.getModel().areBreakpointsActivated() ? ToggleBreakpointsActivatedAction.DEACTIVATE_LABEL : ToggleBreakpointsActivatedAction.ACTIVATE_LABEL); - this.updateEnablement(); - })); - } - - run(): Promise { - return this.debugService.setBreakpointsActivated(!this.debugService.getModel().areBreakpointsActivated()); - } - - protected isEnabled(_: State): boolean { - return !!(this.debugService.getModel().getFunctionBreakpoints().length || this.debugService.getModel().getBreakpoints().length || this.debugService.getModel().getDataBreakpoints().length); - } -} - -export class ReapplyBreakpointsAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.reapplyBreakpointsAction'; - static readonly LABEL = nls.localize('reapplyAllBreakpoints', "Reapply All Breakpoints"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, '', debugService, keybindingService); - this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement())); - } - - run(): Promise { - return this.debugService.setBreakpointsActivated(true); - } - - protected isEnabled(state: State): boolean { - const model = this.debugService.getModel(); - return (state === State.Running || state === State.Stopped) && - ((model.getFunctionBreakpoints().length + model.getBreakpoints().length + model.getExceptionBreakpoints().length + model.getDataBreakpoints().length) > 0); - } -} - -export class AddFunctionBreakpointAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.addFunctionBreakpointAction'; - static readonly LABEL = nls.localize('addFunctionBreakpoint', "Add Function Breakpoint"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, 'debug-action ' + ThemeIcon.asClassName(icons.watchExpressionsAddFuncBreakpoint), debugService, keybindingService); - this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement())); - } - - async run(): Promise { - this.debugService.addFunctionBreakpoint(); - } - - protected isEnabled(_: State): boolean { - return !this.debugService.getViewModel().getSelectedBreakpoint() - && this.debugService.getModel().getFunctionBreakpoints().every(fbp => !!fbp.name); - } -} - -export class AddWatchExpressionAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.addWatchExpression'; - static readonly LABEL = nls.localize('addWatchExpression', "Add Expression"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, 'debug-action ' + ThemeIcon.asClassName(icons.watchExpressionsAdd), debugService, keybindingService); - this._register(this.debugService.getModel().onDidChangeWatchExpressions(() => this.updateEnablement())); - this._register(this.debugService.getViewModel().onDidSelectExpression(() => this.updateEnablement())); - } - - async run(): Promise { - this.debugService.addWatchExpression(); - } - - protected isEnabled(_: State): boolean { - const focusedExpression = this.debugService.getViewModel().getSelectedExpression(); - return this.debugService.getModel().getWatchExpressions().every(we => !!we.name && we !== focusedExpression); - } -} - -export class RemoveAllWatchExpressionsAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.removeAllWatchExpressions'; - static readonly LABEL = nls.localize('removeAllWatchExpressions', "Remove All Expressions"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, 'debug-action ' + ThemeIcon.asClassName(icons.watchExpressionsRemoveAll), debugService, keybindingService); - this._register(this.debugService.getModel().onDidChangeWatchExpressions(() => this.updateEnablement())); - } - - async run(): Promise { - this.debugService.removeWatchExpressions(); - } - - protected isEnabled(_: State): boolean { - return this.debugService.getModel().getWatchExpressions().length > 0; - } -} - -export class FocusSessionAction extends AbstractDebugAction { - static readonly ID = 'workbench.action.debug.focusProcess'; - static readonly LABEL = nls.localize('focusSession', "Focus Session"); - - constructor(id: string, label: string, - @IDebugService debugService: IDebugService, - @IKeybindingService keybindingService: IKeybindingService, - @IEditorService private readonly editorService: IEditorService - ) { - super(id, label, '', debugService, keybindingService); - } - - async run(session: IDebugSession): Promise { - await this.debugService.focusStackFrame(undefined, undefined, session, true); - const stackFrame = this.debugService.getViewModel().focusedStackFrame; - if (stackFrame) { - await stackFrame.openInEditor(this.editorService, true); - } - } -} - -export class CopyValueAction extends Action { - static readonly ID = 'workbench.debug.viewlet.action.copyValue'; - static readonly LABEL = nls.localize('copyValue', "Copy Value"); - - constructor( - id: string, label: string, private value: Variable | Expression, private context: string, - @IDebugService private readonly debugService: IDebugService, - @IClipboardService private readonly clipboardService: IClipboardService - ) { - super(id, label); - this._enabled = (this.value instanceof Expression) || (this.value instanceof Variable && !!this.value.evaluateName); - } - - async run(): Promise { - const stackFrame = this.debugService.getViewModel().focusedStackFrame; - const session = this.debugService.getViewModel().focusedSession; - if (!stackFrame || !session) { - return; - } - - const context = session.capabilities.supportsClipboardContext ? 'clipboard' : this.context; - const toEvaluate = this.value instanceof Variable ? (this.value.evaluateName || this.value.value) : this.value.name; - - try { - const evaluation = await session.evaluate(toEvaluate, stackFrame.frameId, context); - if (evaluation) { - this.clipboardService.writeText(evaluation.body.result); - } - } catch (e) { - this.clipboardService.writeText(typeof this.value === 'string' ? this.value : this.value.value); - } - } -} diff --git a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts index 249f1e69..bf1fc62f 100644 --- a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts @@ -54,11 +54,6 @@ export class AdapterManager implements IAdapterManager { if (!rawAdapter.type || (typeof rawAdapter.type !== 'string')) { added.collector.error(nls.localize('debugNoType', "Debugger 'type' can not be omitted and must be of type 'string'.")); } - if (rawAdapter.enableBreakpointsFor && rawAdapter.enableBreakpointsFor.languageIds) { - rawAdapter.enableBreakpointsFor.languageIds.forEach(modeId => { - this.breakpointModeIdsSet.add(modeId); - }); - } if (rawAdapter.type !== '*') { const existing = this.getDebugger(rawAdapter.type); diff --git a/src/vs/workbench/contrib/debug/browser/debugColors.ts b/src/vs/workbench/contrib/debug/browser/debugColors.ts index df54038d..476f8571 100644 --- a/src/vs/workbench/contrib/debug/browser/debugColors.ts +++ b/src/vs/workbench/contrib/debug/browser/debugColors.ts @@ -4,8 +4,28 @@ *--------------------------------------------------------------------------------------------*/ import { registerColor, foreground, editorInfoForeground, editorWarningForeground, errorForeground, badgeBackground, badgeForeground, listDeemphasizedForeground, contrastBorder, inputBorder } from 'vs/platform/theme/common/colorRegistry'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { Color } from 'vs/base/common/color'; +import { localize } from 'vs/nls'; +import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; + +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 function registerColors() { @@ -28,6 +48,62 @@ export function registerColors() { const debugConsoleSourceForeground = registerColor('debugConsole.sourceForeground', { dark: foreground, light: foreground, hc: foreground }, 'Foreground color for source filenames in debug REPL console.'); const debugConsoleInputIconForeground = registerColor('debugConsoleInputIcon.foreground', { dark: foreground, light: foreground, hc: foreground }, 'Foreground color for debug console input marker icon.'); + + + const debugIconPauseForeground = registerColor('debugIcon.pauseForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' + }, localize('debugIcon.pauseForeground', "Debug toolbar icon for pause.")); + + const debugIconStopForeground = registerColor('debugIcon.stopForeground', { + dark: '#F48771', + light: '#A1260D', + hc: '#F48771' + }, localize('debugIcon.stopForeground', "Debug toolbar icon for stop.")); + + const debugIconDisconnectForeground = registerColor('debugIcon.disconnectForeground', { + dark: '#F48771', + light: '#A1260D', + hc: '#F48771' + }, localize('debugIcon.disconnectForeground', "Debug toolbar icon for disconnect.")); + + const debugIconRestartForeground = registerColor('debugIcon.restartForeground', { + dark: '#89D185', + light: '#388A34', + hc: '#89D185' + }, localize('debugIcon.restartForeground', "Debug toolbar icon for restart.")); + + const debugIconStepOverForeground = registerColor('debugIcon.stepOverForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' + }, localize('debugIcon.stepOverForeground', "Debug toolbar icon for step over.")); + + const debugIconStepIntoForeground = registerColor('debugIcon.stepIntoForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' + }, localize('debugIcon.stepIntoForeground', "Debug toolbar icon for step into.")); + + const debugIconStepOutForeground = registerColor('debugIcon.stepOutForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' + }, localize('debugIcon.stepOutForeground', "Debug toolbar icon for step over.")); + + const debugIconContinueForeground = registerColor('debugIcon.continueForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' + }, localize('debugIcon.continueForeground', "Debug toolbar icon for continue.")); + + const debugIconStepBackForeground = registerColor('debugIcon.stepBackForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' + }, localize('debugIcon.stepBackForeground', "Debug toolbar icon for step back.")); + registerThemingParticipant((theme, collector) => { // All these colours provide a default value so they will never be undefined, hence the `!` const badgeBackgroundColor = theme.getColor(badgeBackground)!; @@ -186,5 +262,55 @@ export function registerColors() { } `); } + + const debugIconStartColor = theme.getColor(debugIconStartForeground); + if (debugIconStartColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStart)} { color: ${debugIconStartColor} !important; }`); + } + + const debugIconPauseColor = theme.getColor(debugIconPauseForeground); + if (debugIconPauseColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugPause)} { color: ${debugIconPauseColor} !important; }`); + } + + const debugIconStopColor = theme.getColor(debugIconStopForeground); + if (debugIconStopColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStop)} { color: ${debugIconStopColor} !important; }`); + } + + const debugIconDisconnectColor = theme.getColor(debugIconDisconnectForeground); + if (debugIconDisconnectColor) { + collector.addRule(`.monaco-workbench .debug-view-content ${ThemeIcon.asCSSSelector(icons.debugDisconnect)}, .monaco-workbench .debug-toolbar ${ThemeIcon.asCSSSelector(icons.debugDisconnect)} { color: ${debugIconDisconnectColor} !important; }`); + } + + const debugIconRestartColor = theme.getColor(debugIconRestartForeground); + if (debugIconRestartColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugRestart)}, .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugRestartFrame)} { color: ${debugIconRestartColor} !important; }`); + } + + const debugIconStepOverColor = theme.getColor(debugIconStepOverForeground); + if (debugIconStepOverColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepOver)} { color: ${debugIconStepOverColor} !important; }`); + } + + const debugIconStepIntoColor = theme.getColor(debugIconStepIntoForeground); + if (debugIconStepIntoColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepInto)} { color: ${debugIconStepIntoColor} !important; }`); + } + + const debugIconStepOutColor = theme.getColor(debugIconStepOutForeground); + if (debugIconStepOutColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepOut)} { color: ${debugIconStepOutColor} !important; }`); + } + + const debugIconContinueColor = theme.getColor(debugIconContinueForeground); + if (debugIconContinueColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugContinue)}, .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugReverseContinue)} { color: ${debugIconContinueColor} !important; }`); + } + + const debugIconStepBackColor = theme.getColor(debugIconStepBackForeground); + if (debugIconStepBackColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepBack)} { color: ${debugIconStepBackColor} !important; }`); + } }); } diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 47aaedfb..3b353cd6 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, IDebugConfiguration, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, REPL_VIEW_ID } 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, IConfig, IStackFrame, IThread, IDebugSession, CONTEXT_DEBUG_STATE, IDebugConfiguration, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, REPL_VIEW_ID, CONTEXT_DEBUGGERS_AVAILABLE, State, getStateLabel, CONTEXT_BREAKPOINT_INPUT_FOCUSED } 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'; @@ -23,12 +23,13 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { InputFocusedContext } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { PanelFocusContext } from 'vs/workbench/common/panel'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IViewsService } from 'vs/workbench/common/views'; +import { deepClone } from 'vs/base/common/objects'; export const ADD_CONFIGURATION_ID = 'debug.addConfiguration'; export const TOGGLE_INLINE_BREAKPOINT_ID = 'editor.debug.action.toggleInlineBreakpoint'; @@ -47,6 +48,13 @@ export const RESTART_FRAME_ID = 'workbench.action.debug.restartFrame'; export const CONTINUE_ID = 'workbench.action.debug.continue'; export const FOCUS_REPL_ID = 'workbench.debug.action.focusRepl'; export const JUMP_TO_CURSOR_ID = 'debug.jumpToCursor'; +export const FOCUS_SESSION_ID = 'workbench.action.debug.focusProcess'; +export const SELECT_AND_START_ID = 'workbench.action.debug.selectandstart'; +export const DEBUG_CONFIGURE_COMMAND_ID = 'workbench.action.debug.configure'; +export const DEBUG_START_COMMAND_ID = 'workbench.action.debug.start'; +export const DEBUG_RUN_COMMAND_ID = 'workbench.action.debug.run'; +export const EDIT_EXPRESSION_COMMAND_ID = 'debug.renameWatchExpression'; +export const REMOVE_EXPRESSION_COMMAND_ID = 'debug.removeWatchExpression'; export const RESTART_LABEL = nls.localize('restartDebug', "Restart"); export const STEP_OVER_LABEL = nls.localize('stepOverDebug', "Step Over"); @@ -56,6 +64,11 @@ export const PAUSE_LABEL = nls.localize('pauseDebug', "Pause"); export const DISCONNECT_LABEL = nls.localize('disconnect', "Disconnect"); export const STOP_LABEL = nls.localize('stop', "Stop"); export const CONTINUE_LABEL = nls.localize('continueDebug', "Continue"); +export const FOCUS_SESSION_LABEL = nls.localize('focusSession', "Focus Session"); +export const SELECT_AND_START_LABEL = nls.localize('selectAndStartDebugging', "Select and Start Debugging"); +export const DEBUG_CONFIGURE_LABEL = nls.localize('openLaunchJson', "Open {0}", 'launch.json'); +export const DEBUG_START_LABEL = nls.localize('startDebug', "Start Debugging"); +export const DEBUG_RUN_LABEL = nls.localize('startWithoutDebugging', "Start Without Debugging"); interface CallStackContext { sessionId: string; @@ -322,7 +335,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: CONTINUE_ID, - weight: KeybindingWeight.WorkbenchContrib, + weight: KeybindingWeight.WorkbenchContrib + 10, // Use a stronger weight to get priority over start debugging F5 shortcut primary: KeyCode.F5, when: CONTEXT_IN_DEBUG_MODE, handler: (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => { @@ -346,6 +359,53 @@ export function registerCommands(): void { } }); + CommandsRegistry.registerCommand({ + id: FOCUS_SESSION_ID, + handler: async (accessor: ServicesAccessor, session: IDebugSession) => { + const debugService = accessor.get(IDebugService); + const editorService = accessor.get(IEditorService); + await debugService.focusStackFrame(undefined, undefined, session, true); + const stackFrame = debugService.getViewModel().focusedStackFrame; + if (stackFrame) { + await stackFrame.openInEditor(editorService, true); + } + } + }); + + CommandsRegistry.registerCommand({ + id: SELECT_AND_START_ID, + handler: async (accessor: ServicesAccessor) => { + const quickInputService = accessor.get(IQuickInputService); + quickInputService.quickAccess.show('debug '); + } + }); + + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: DEBUG_START_COMMAND_ID, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.F5, + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing))), + handler: async (accessor: ServicesAccessor, debugStartOptions?: { noDebug: boolean }) => { + const debugService = accessor.get(IDebugService); + let { launch, name, getConfig } = debugService.getConfigurationManager().selectedConfiguration; + const config = await getConfig(); + const clonedConfig = deepClone(config); + await debugService.startDebugging(launch, clonedConfig || name, { noDebug: debugStartOptions && debugStartOptions.noDebug }); + } + }); + + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: DEBUG_RUN_COMMAND_ID, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.F5, + mac: { primary: KeyMod.WinCtrl | KeyCode.F5 }, + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing))), + handler: async (accessor: ServicesAccessor) => { + const commandService = accessor.get(ICommandService); + await commandService.executeCommand(DEBUG_START_COMMAND_ID, { noDebug: true }); + } + }); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'debug.toggleBreakpoint', weight: KeybindingWeight.WorkbenchContrib + 5, @@ -389,22 +449,27 @@ export function registerCommands(): void { }); KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'debug.renameWatchExpression', + id: EDIT_EXPRESSION_COMMAND_ID, weight: KeybindingWeight.WorkbenchContrib + 5, when: CONTEXT_WATCH_EXPRESSIONS_FOCUSED, primary: KeyCode.F2, mac: { primary: KeyCode.Enter }, - handler: (accessor) => { - const listService = accessor.get(IListService); + handler: (accessor: ServicesAccessor, expression: Expression | unknown) => { const debugService = accessor.get(IDebugService); - const focused = listService.lastFocusedList; - - if (focused) { - const elements = focused.getFocus(); - if (Array.isArray(elements) && elements[0] instanceof Expression) { - debugService.getViewModel().setSelectedExpression(elements[0]); + if (!(expression instanceof Expression)) { + const listService = accessor.get(IListService); + const focused = listService.lastFocusedList; + if (focused) { + const elements = focused.getFocus(); + if (Array.isArray(elements) && elements[0] instanceof Expression) { + expression = elements[0]; + } } } + + if (expression instanceof Expression) { + debugService.getViewModel().setSelectedExpression(expression); + } } }); @@ -429,16 +494,21 @@ export function registerCommands(): void { }); KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'debug.removeWatchExpression', + id: REMOVE_EXPRESSION_COMMAND_ID, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_EXPRESSION_SELECTED.toNegated()), primary: KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace }, - handler: (accessor) => { - const listService = accessor.get(IListService); + handler: (accessor: ServicesAccessor, expression: Expression | unknown) => { const debugService = accessor.get(IDebugService); - const focused = listService.lastFocusedList; + if (expression instanceof Expression) { + debugService.removeWatchExpressions(expression.getId()); + return; + } + + const listService = accessor.get(IListService); + const focused = listService.lastFocusedList; if (focused) { let elements = focused.getFocus(); if (Array.isArray(elements) && elements[0] instanceof Expression) { @@ -455,7 +525,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'debug.removeBreakpoint', weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_SELECTED.toNegated()), + when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_INPUT_FOCUSED.toNegated()), primary: KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace }, handler: (accessor) => { diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index d8ba1d55..bb92347d 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; 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 { registerEditorAction, EditorAction, IActionOptions, EditorAction2 } from 'vs/editor/browser/editorExtensions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE, State, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, REPL_VIEW_ID, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, WATCH_VIEW_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_EXCEPTION_WIDGET_VISIBLE } from 'vs/workbench/contrib/debug/common/debug'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -24,24 +24,35 @@ import { Position } from 'vs/editor/common/core/position'; import { URI } from 'vs/base/common/uri'; import { IDisposable } from 'vs/base/common/lifecycle'; import { raceTimeout } from 'vs/base/common/async'; +import { registerAction2, MenuId } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -export const TOGGLE_BREAKPOINT_ID = 'editor.debug.action.toggleBreakpoint'; -class ToggleBreakpointAction extends EditorAction { +class ToggleBreakpointAction extends EditorAction2 { constructor() { super({ - id: TOGGLE_BREAKPOINT_ID, - label: nls.localize('toggleBreakpointAction', "Debug: Toggle Breakpoint"), - alias: 'Debug: Toggle Breakpoint', + id: 'editor.debug.action.toggleBreakpoint', + title: { + value: nls.localize('toggleBreakpointAction', "Debug: Toggle Breakpoint"), + original: 'Toggle Breakpoint', + mnemonicTitle: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breakpoint") + }, + f1: true, precondition: CONTEXT_DEBUGGERS_AVAILABLE, - kbOpts: { - kbExpr: EditorContextKeys.editorTextFocus, + keybinding: { + when: EditorContextKeys.editorTextFocus, primary: KeyCode.F9, weight: KeybindingWeight.EditorContrib + }, + menu: { + when: CONTEXT_DEBUGGERS_AVAILABLE, + id: MenuId.MenubarDebugMenu, + group: '4_new_breakpoint', + order: 1 } }); } - async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { + async runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]): Promise { if (editor.hasModel()) { const debugService = accessor.get(IDebugService); const modelUri = editor.getModel().uri; @@ -49,33 +60,39 @@ class ToggleBreakpointAction extends EditorAction { // Does not account for multi line selections, Set to remove multiple cursor on the same line const lineNumbers = [...new Set(editor.getSelections().map(s => s.getPosition().lineNumber))]; - return Promise.all(lineNumbers.map(line => { + await Promise.all(lineNumbers.map(async line => { const bps = debugService.getModel().getBreakpoints({ lineNumber: line, uri: modelUri }); if (bps.length) { - return Promise.all(bps.map(bp => debugService.removeBreakpoints(bp.getId()))); + await Promise.all(bps.map(bp => debugService.removeBreakpoints(bp.getId()))); } else if (canSet) { - return (debugService.addBreakpoints(modelUri, [{ lineNumber: line }])); - } else { - return []; + await debugService.addBreakpoints(modelUri, [{ lineNumber: line }]); } })); } } } -export const TOGGLE_CONDITIONAL_BREAKPOINT_ID = 'editor.debug.action.conditionalBreakpoint'; -class ConditionalBreakpointAction extends EditorAction { - +class ConditionalBreakpointAction extends EditorAction2 { constructor() { super({ - id: TOGGLE_CONDITIONAL_BREAKPOINT_ID, - label: nls.localize('conditionalBreakpointEditorAction', "Debug: Add Conditional Breakpoint..."), - alias: 'Debug: Add Conditional Breakpoint...', - precondition: CONTEXT_DEBUGGERS_AVAILABLE + id: 'editor.debug.action.conditionalBreakpoint', + title: { + value: nls.localize('conditionalBreakpointEditorAction', "Debug: Add Conditional Breakpoint..."), + original: 'Debug: Add Conditional Breakpoint...', + mnemonicTitle: nls.localize({ key: 'miConditionalBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Conditional Breakpoint...") + }, + f1: true, + precondition: CONTEXT_DEBUGGERS_AVAILABLE, + menu: { + id: MenuId.MenubarNewBreakpointMenu, + group: '1_breakpoints', + order: 1, + when: CONTEXT_DEBUGGERS_AVAILABLE + } }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]): void { const debugService = accessor.get(IDebugService); const position = editor.getPosition(); @@ -85,19 +102,28 @@ class ConditionalBreakpointAction extends EditorAction { } } -export const ADD_LOG_POINT_ID = 'editor.debug.action.addLogPoint'; -class LogPointAction extends EditorAction { +class LogPointAction extends EditorAction2 { constructor() { super({ - id: ADD_LOG_POINT_ID, - label: nls.localize('logPointEditorAction', "Debug: Add Logpoint..."), - alias: 'Debug: Add Logpoint...', - precondition: CONTEXT_DEBUGGERS_AVAILABLE + id: 'editor.debug.action.addLogPoint', + title: { + value: nls.localize('logPointEditorAction', "Debug: Add Logpoint..."), + original: 'Debug: Add Logpoint...', + mnemonicTitle: nls.localize({ key: 'miLogPoint', comment: ['&& denotes a mnemonic'] }, "&&Logpoint...") + }, + precondition: CONTEXT_DEBUGGERS_AVAILABLE, + f1: true, + menu: { + id: MenuId.MenubarNewBreakpointMenu, + group: '1_breakpoints', + order: 4, + when: CONTEXT_DEBUGGERS_AVAILABLE + } }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]): void { const debugService = accessor.get(IDebugService); const position = editor.getPosition(); @@ -476,9 +502,9 @@ class CloseExceptionWidgetAction extends EditorAction { } export function registerEditorActions(): void { - registerEditorAction(ToggleBreakpointAction); - registerEditorAction(ConditionalBreakpointAction); - registerEditorAction(LogPointAction); + registerAction2(ToggleBreakpointAction); + registerAction2(ConditionalBreakpointAction); + registerAction2(LogPointAction); registerEditorAction(RunToCursorAction); registerEditorAction(StepIntoTargetsAction); registerEditorAction(SelectionToReplAction); diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index dbd53aa5..f3fa36a2 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -23,7 +23,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ICommandService } from 'vs/platform/commands/common/commands'; import { IDebugEditorContribution, IDebugService, State, IStackFrame, IDebugConfiguration, IExpression, IExceptionInfo, IDebugSession, CONTEXT_EXCEPTION_WIDGET_VISIBLE } from 'vs/workbench/contrib/debug/common/debug'; import { ExceptionWidget } from 'vs/workbench/contrib/debug/browser/exceptionWidget'; -import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; +import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; import { Position } from 'vs/editor/common/core/position'; import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; import { memoize, createMemoizer } from 'vs/base/common/decorators'; @@ -339,7 +339,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { if (this.hoverRange) { this.showHover(this.hoverRange, false); } - }, hoverOption.delay); + }, hoverOption.delay * 2); this.toDispose.push(scheduler); return scheduler; @@ -432,7 +432,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { this.closeExceptionWidget(); } else if (sameUri) { const exceptionInfo = await focusedSf.thread.exceptionInfo; - if (exceptionInfo && exceptionSf.range.startLineNumber && exceptionSf.range.startColumn) { + if (exceptionInfo) { this.showExceptionWidget(exceptionInfo, this.debugService.getViewModel().focusedSession, exceptionSf.range.startLineNumber, exceptionSf.range.startColumn); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugHover.ts b/src/vs/workbench/contrib/debug/browser/debugHover.ts index d828f5bf..3bbddfa9 100644 --- a/src/vs/workbench/contrib/debug/browser/debugHover.ts +++ b/src/vs/workbench/contrib/debug/browser/debugHover.ts @@ -105,7 +105,7 @@ export class DebugHoverWidget implements IContentWidget { this.treeContainer = dom.append(this.complexValueContainer, $('.debug-hover-tree')); this.treeContainer.setAttribute('role', 'tree'); const tip = dom.append(this.complexValueContainer, $('.tip')); - tip.textContent = nls.localize('quickTip', 'Hold {0} key to switch to editor language hover', isMacintosh ? 'Option' : 'Alt'); + tip.textContent = nls.localize({ key: 'quickTip', comment: ['"switch to editor language hover" means to show the programming language hover widget instead of the debug hover'] }, 'Hold {0} key to switch to editor language hover', isMacintosh ? 'Option' : 'Alt'); const dataSource = new DebugHoverDataSource(); this.tree = >this.instantiationService.createInstance(WorkbenchAsyncDataTree, 'DebugHover', this.treeContainer, new DebugHoverDelegate(), [this.instantiationService.createInstance(VariablesRenderer)], diff --git a/src/vs/workbench/contrib/debug/browser/debugIcons.ts b/src/vs/workbench/contrib/debug/browser/debugIcons.ts index b2203e38..b031cd58 100644 --- a/src/vs/workbench/contrib/debug/browser/debugIcons.ts +++ b/src/vs/workbench/contrib/debug/browser/debugIcons.ts @@ -15,25 +15,37 @@ export const callStackViewIcon = registerIcon('callstack-view-icon', Codicon.deb export const breakpointsViewIcon = registerIcon('breakpoints-view-icon', Codicon.debugAlt, localize('breakpointsViewIcon', 'View icon of the breakpoints view.')); export const loadedScriptsViewIcon = registerIcon('loaded-scripts-view-icon', Codicon.debugAlt, localize('loadedScriptsViewIcon', 'View icon of the loaded scripts view.')); -export const debugBreakpoint = registerIcon('debug-breakpoint', Codicon.debugBreakpoint, localize('debugBreakpoint', 'Icon for breakpoints.')); -export const debugBreakpointDisabled = registerIcon('debug-breakpoint-disabled', Codicon.debugBreakpointDisabled, localize('debugBreakpointDisabled', 'Icon for disabled breakpoints.')); -export const debugBreakpointUnverified = registerIcon('debug-breakpoint-unverified', Codicon.debugBreakpointUnverified, localize('debugBreakpointUnverified', 'Icon for unverified breakpoints.')); -export const debugBreakpointHint = registerIcon('debug-hint', Codicon.debugHint, localize('debugBreakpointHint', 'Icon for breakpoint hints shown on hover in editor glyph margin.')); -export const debugBreakpointFunction = registerIcon('debug-breakpoint-function', Codicon.debugBreakpointFunction, localize('debugBreakpointFunction', 'Icon for function breakpoints.')); -export const debugBreakpointFunctionUnverified = registerIcon('debug-breakpoint-function-unverified', Codicon.debugBreakpointFunctionUnverified, localize('debugBreakpointFunctionUnverified', 'Icon for unverified function breakpoints.')); -export const debugBreakpointFunctionDisabled = registerIcon('debug-breakpoint-function-disabled', Codicon.debugBreakpointFunctionDisabled, localize('debugBreakpointFunctionDisabled', 'Icon for disabled function breakpoints.')); +export const breakpoint = { + regular: registerIcon('debug-breakpoint', Codicon.debugBreakpoint, localize('debugBreakpoint', 'Icon for breakpoints.')), + disabled: registerIcon('debug-breakpoint-disabled', Codicon.debugBreakpointDisabled, localize('debugBreakpointDisabled', 'Icon for disabled breakpoints.')), + unverified: registerIcon('debug-breakpoint-unverified', Codicon.debugBreakpointUnverified, localize('debugBreakpointUnverified', 'Icon for unverified breakpoints.')) +}; +export const functionBreakpoint = { + regular: registerIcon('debug-breakpoint-function', Codicon.debugBreakpointFunction, localize('debugBreakpointFunction', 'Icon for function breakpoints.')), + disabled: registerIcon('debug-breakpoint-function-disabled', Codicon.debugBreakpointFunctionDisabled, localize('debugBreakpointFunctionDisabled', 'Icon for disabled function breakpoints.')), + unverified: registerIcon('debug-breakpoint-function-unverified', Codicon.debugBreakpointFunctionUnverified, localize('debugBreakpointFunctionUnverified', 'Icon for unverified function breakpoints.')) +}; +export const conditionalBreakpoint = { + regular: registerIcon('debug-breakpoint-conditional', Codicon.debugBreakpointConditional, localize('debugBreakpointConditional', 'Icon for conditional breakpoints.')), + disabled: registerIcon('debug-breakpoint-conditional-disabled', Codicon.debugBreakpointConditionalDisabled, localize('debugBreakpointConditionalDisabled', 'Icon for disabled conditional breakpoints.')), + unverified: registerIcon('debug-breakpoint-conditional-unverified', Codicon.debugBreakpointConditionalUnverified, localize('debugBreakpointConditionalUnverified', 'Icon for unverified conditional breakpoints.')) +}; +export const dataBreakpoint = { + regular: registerIcon('debug-breakpoint-data', Codicon.debugBreakpointData, localize('debugBreakpointData', 'Icon for data breakpoints.')), + disabled: registerIcon('debug-breakpoint-data-disabled', Codicon.debugBreakpointDataDisabled, localize('debugBreakpointDataDisabled', 'Icon for disabled data breakpoints.')), + unverified: registerIcon('debug-breakpoint-data-unverified', Codicon.debugBreakpointDataUnverified, localize('debugBreakpointDataUnverified', 'Icon for unverified data breakpoints.')), +}; +export const logBreakpoint = { + regular: registerIcon('debug-breakpoint-log', Codicon.debugBreakpointLog, localize('debugBreakpointLog', 'Icon for log breakpoints.')), + disabled: registerIcon('debug-breakpoint-log-disabled', Codicon.debugBreakpointLogDisabled, localize('debugBreakpointLogDisabled', 'Icon for disabled log breakpoint.')), + unverified: registerIcon('debug-breakpoint-log-unverified', Codicon.debugBreakpointLogUnverified, localize('debugBreakpointLogUnverified', 'Icon for unverified log breakpoints.')), +}; +export const debugBreakpointHint = registerIcon('debug-hint', Codicon.debugHint, localize('debugBreakpointHint', 'Icon for breakpoint hints shown on hover in editor glyph margin.')); export const debugBreakpointUnsupported = registerIcon('debug-breakpoint-unsupported', Codicon.debugBreakpointUnsupported, localize('debugBreakpointUnsupported', 'Icon for unsupported breakpoints.')); -export const debugBreakpointConditionalUnverified = registerIcon('debug-breakpoint-conditional-unverified', Codicon.debugBreakpointConditionalUnverified, localize('debugBreakpointConditionalUnverified', 'Icon for unverified conditional breakpoints.')); -export const debugBreakpointConditional = registerIcon('debug-breakpoint-conditional', Codicon.debugBreakpointConditional, localize('debugBreakpointConditional', 'Icon for conditional breakpoints.')); -export const debugBreakpointConditionalDisabled = registerIcon('debug-breakpoint-conditional-disabled', Codicon.debugBreakpointConditionalDisabled, localize('debugBreakpointConditionalDisabled', 'Icon for disabled conditional breakpoints.')); -export const debugBreakpointDataUnverified = registerIcon('debug-breakpoint-data-unverified', Codicon.debugBreakpointDataUnverified, localize('debugBreakpointDataUnverified', 'Icon for unverified data breakpoints.')); -export const debugBreakpointData = registerIcon('debug-breakpoint-data', Codicon.debugBreakpointData, localize('debugBreakpointData', 'Icon for data breakpoints.')); -export const debugBreakpointDataDisabled = registerIcon('debug-breakpoint-data-disabled', Codicon.debugBreakpointDataDisabled, localize('debugBreakpointDataDisabled', 'Icon for disabled data breakpoints.')); -export const debugBreakpointLogUnverified = registerIcon('debug-breakpoint-log-unverified', Codicon.debugBreakpointLogUnverified, localize('debugBreakpointLogUnverified', 'Icon for unverified log breakpoints.')); -export const debugBreakpointLog = registerIcon('debug-breakpoint-log', Codicon.debugBreakpointLog, localize('debugBreakpointLog', 'Icon for log breakpoints.')); -export const debugBreakpointLogDisabled = registerIcon('debug-breakpoint-log-disabled', Codicon.debugBreakpointLogDisabled, localize('debugBreakpointLogDisabled', 'Icon for disabled log breakpoint.')); +export const allBreakpoints = [breakpoint, functionBreakpoint, conditionalBreakpoint, dataBreakpoint, logBreakpoint]; + export const debugStackframe = registerIcon('debug-stackframe', Codicon.debugStackframe, localize('debugStackframe', 'Icon for a stackframe shown in the editor glyph margin.')); export const debugStackframeFocused = registerIcon('debug-stackframe-focused', Codicon.debugStackframeFocused, localize('debugStackframeFocused', 'Icon for a focused stackframe shown in the editor glyph margin.')); diff --git a/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts b/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts index 2b6f530c..3c920d13 100644 --- a/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts +++ b/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts @@ -115,7 +115,8 @@ export class StartDebugQuickAccessProvider extends PickerQuickAccessProvider { const pick = await provider.pick(); if (pick) { - await configManager.selectConfiguration(pick.launch, pick.config.name, pick.config, { type: pick.config.type }); + // Use the type of the provider, not of the config since config sometimes have subtypes (for example "node-terminal") + await configManager.selectConfiguration(pick.launch, pick.config.name, pick.config, { type: provider.type }); this.debugService.startDebugging(pick.launch, pick.config); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index e05a471f..7631d540 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -17,7 +17,6 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files'; import { DebugModel, FunctionBreakpoint, Breakpoint, DataBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { ViewModel } from 'vs/workbench/contrib/debug/common/debugViewModel'; -import * as debugactions from 'vs/workbench/contrib/debug/browser/debugActions'; import { ConfigurationManager } from 'vs/workbench/contrib/debug/browser/debugConfigurationManager'; import { VIEWLET_ID as EXPLORER_VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -25,7 +24,6 @@ import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/bro import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { parse, getFirstFrame } from 'vs/base/common/console'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IAction, Action } from 'vs/base/common/actions'; @@ -48,9 +46,9 @@ import { DebugTelemetry } from 'vs/workbench/contrib/debug/common/debugTelemetry import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { AdapterManager } from 'vs/workbench/contrib/debug/browser/debugAdapterManager'; import { ITextModel } from 'vs/editor/common/model'; +import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; export class DebugService implements IDebugService { declare readonly _serviceBrand: undefined; @@ -96,8 +94,7 @@ export class DebugService implements IDebugService { @IExtensionHostDebugService private readonly extensionHostDebugService: IExtensionHostDebugService, @IActivityService private readonly activityService: IActivityService, @ICommandService private readonly commandService: ICommandService, - @IQuickInputService private readonly quickInputService: IQuickInputService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService + @IQuickInputService private readonly quickInputService: IQuickInputService ) { this.toDispose = []; @@ -149,16 +146,6 @@ export class DebugService implements IDebugService { session.disconnect(); } })); - this.toDispose.push(this.extensionHostDebugService.onLogToSession(event => { - const session = this.model.getSession(event.sessionId, true); - if (session) { - // extension logged output -> show it in REPL - const sev = event.log.severity === 'warn' ? severity.Warning : event.log.severity === 'error' ? severity.Error : severity.Info; - const { args, stack } = parse(event.log); - const frame = !!stack ? getFirstFrame(stack) : undefined; - session.logToRepl(sev, args, frame); - } - })); this.toDispose.push(this.viewModel.onDidFocusStackFrame(() => { this.onStateChange(); @@ -290,6 +277,11 @@ export class DebugService implements IDebugService { await this.extensionService.activateByEvent('onDebug'); if (!options?.parentSession) { await this.editorService.saveAll(); + const activeEditor = this.editorService.activeEditorPane; + if (activeEditor) { + // Make sure to save the active editor in case it is in untitled file it wont be saved as part of saveAll #111850 + await this.editorService.save({ editor: activeEditor.input, groupId: activeEditor.group.id }); + } } await this.configurationService.reloadConfiguration(launch ? launch.workspace : undefined); await this.extensionService.whenInstalledExtensionsRegistered(); @@ -302,15 +294,6 @@ export class DebugService implements IDebugService { if (typeof configOrName === 'string' && launch) { config = launch.getConfiguration(configOrName); compound = launch.getCompound(configOrName); - - const sessions = this.model.getSessions(); - const alreadyRunningMessage = nls.localize('configurationAlreadyRunning', "There is already a debug configuration \"{0}\" running.", configOrName); - if (sessions.some(s => (s.configuration.name === configOrName && s.root === launch.workspace) && (!launch || !launch.workspace || !s.root || this.uriIdentityService.extUri.isEqual(s.root.uri, launch.workspace.uri)))) { - throw new Error(alreadyRunningMessage); - } - if (compound && compound.configurations && sessions.some(p => compound!.configurations.indexOf(p.configuration.name) !== -1)) { - throw new Error(alreadyRunningMessage); - } } else if (typeof configOrName !== 'string') { config = configOrName; } @@ -784,7 +767,7 @@ export class DebugService implements IDebugService { } private async showError(message: string, errorActions: ReadonlyArray = []): Promise { - const configureAction = this.instantiationService.createInstance(debugactions.ConfigureAction, debugactions.ConfigureAction.ID, debugactions.ConfigureAction.LABEL); + const configureAction = new Action(DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL, undefined, true, () => this.commandService.executeCommand(DEBUG_CONFIGURE_COMMAND_ID)); const actions = [...errorActions, configureAction]; const { choice } = await this.dialogService.show(severity.Error, message, actions.map(a => a.label).concat(nls.localize('cancel', "Cancel")), { cancelId: actions.length }); if (choice < actions.length) { @@ -917,12 +900,11 @@ export class DebugService implements IDebugService { } addFunctionBreakpoint(name?: string, id?: string): void { - const newFunctionBreakpoint = this.model.addFunctionBreakpoint(name || '', id); - this.viewModel.setSelectedBreakpoint(newFunctionBreakpoint); + this.model.addFunctionBreakpoint(name || '', id); } - async renameFunctionBreakpoint(id: string, newFunctionName: string): Promise { - this.model.renameFunctionBreakpoint(id, newFunctionName); + async updateFunctionBreakpoint(id: string, update: { name?: string, hitCondition?: string, condition?: string }): Promise { + this.model.updateFunctionBreakpoint(id, update); this.debugStorage.storeBreakpoints(this.model); await this.sendFunctionBreakpoints(); } @@ -946,6 +928,11 @@ export class DebugService implements IDebugService { await this.sendDataBreakpoints(); } + setExceptionBreakpoints(data: DebugProtocol.ExceptionBreakpointsFilter[]): void { + this.model.setExceptionBreakpoints(data); + this.debugStorage.storeBreakpoints(this.model); + } + async setExceptionBreakpointCondition(exceptionBreakpoint: IExceptionBreakpoint, condition: string | undefined): Promise { this.model.setExceptionBreakpointCondition(exceptionBreakpoint, condition); this.debugStorage.storeBreakpoints(this.model); diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 9b8c75af..ec549fd0 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -64,7 +64,7 @@ export class DebugSession implements IDebugSession { private readonly _onDidChangeREPLElements = new Emitter(); - private name: string | undefined; + private _name: string | undefined; private readonly _onDidChangeName = new Emitter(); constructor( @@ -146,15 +146,18 @@ export class DebugSession implements IDebugSession { getLabel(): string { const includeRoot = this.workspaceContextService.getWorkspace().folders.length > 1; - const name = this.name || this.configuration.name; - return includeRoot && this.root ? `${name} (${resources.basenameOrAuthority(this.root.uri)})` : name; + return includeRoot && this.root ? `${this.name} (${resources.basenameOrAuthority(this.root.uri)})` : this.name; } setName(name: string): void { - this.name = name; + this._name = name; this._onDidChangeName.fire(name); } + get name(): string { + return this._name || this.configuration.name; + } + get state(): State { if (!this.initialized) { return State.Initializing; @@ -253,7 +256,7 @@ export class DebugSession implements IDebugSession { this.initialized = true; this._onDidChangeState.fire(); - this.model.setExceptionBreakpoints((this.raw && this.raw.capabilities.exceptionBreakpointFilters) || []); + this.debugService.setExceptionBreakpoints((this.raw && this.raw.capabilities.exceptionBreakpointFilters) || []); } catch (err) { this.initialized = true; this._onDidChangeState.fire(); @@ -1015,8 +1018,8 @@ export class DebugSession implements IDebugSession { this._onDidProgressEnd.fire(event); })); this.rawListeners.push(this.raw.onDidInvalidated(async event => { - if (!(event.body.areas && event.body.areas.length === 1 && event.body.areas[0] === 'variables')) { - // If invalidated event only requires to update variables, do that, otherwise refatch threads https://github.com/microsoft/vscode/issues/106745 + if (!(event.body.areas && event.body.areas.length === 1 && (event.body.areas[0] === 'variables' || event.body.areas[0] === 'watch'))) { + // If invalidated event only requires to update variables or watch, do that, otherwise refatch threads https://github.com/microsoft/vscode/issues/106745 this.cancelAllRequests(); this.model.clearThreads(this.getId(), true); await this.fetchThreads(this.stoppedDetails); diff --git a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts index 689328eb..cac07a98 100644 --- a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts +++ b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts @@ -169,12 +169,18 @@ export class DebugTaskRunner { return taskPromise.then(withUndefinedAsNull); }); - return new Promise((c, e) => { + return new Promise(async (c, e) => { + const waitForInput = new Promise(resolve => once(e => (e.kind === TaskEventKind.AcquiredInput) && e.taskId === task._id, this.taskService.onDidStateChange)(() => { + resolve(); + })); + promise.then(result => { taskStarted = true; c(result); }, error => e(error)); + await waitForInput; + setTimeout(() => { if (!taskStarted) { const errorMessage = typeof taskId === 'string' diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 8f62ac8d..cfb91f7a 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -8,28 +8,30 @@ import * as errors from 'vs/base/common/errors'; import * as browser from 'vs/base/browser/browser'; import * as dom from 'vs/base/browser/dom'; import * as arrays from 'vs/base/common/arrays'; +import { localize } from 'vs/nls'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { IAction, IRunEvent, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, Separator } from 'vs/base/common/actions'; +import { IAction, IRunEvent, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IDebugConfiguration, IDebugService, State } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugConfiguration, IDebugService, State, CONTEXT_DEBUG_STATE, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; import { FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems'; -import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { registerThemingParticipant, IThemeService, Themable, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { registerColor, contrastBorder, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; -import { localize } from 'vs/nls'; +import { IThemeService, Themable, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { contrastBorder, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { createAndFillInActionBarActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IMenu, IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { FocusSessionAction } from 'vs/workbench/contrib/debug/browser/debugActions'; +import { createActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenu, IMenuService, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { IContextKeyService, ContextKeyExpression, ContextKeyExpr, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { debugToolBarBackground, debugToolBarBorder } from 'vs/workbench/contrib/debug/browser/debugColors'; +import { URI } from 'vs/base/common/uri'; +import { CONTINUE_LABEL, CONTINUE_ID, PAUSE_ID, STOP_ID, DISCONNECT_ID, STEP_OVER_ID, STEP_INTO_ID, RESTART_SESSION_ID, STEP_OUT_ID, STEP_BACK_ID, REVERSE_CONTINUE_ID, RESTART_LABEL, STEP_OUT_LABEL, STEP_INTO_LABEL, STEP_OVER_LABEL, DISCONNECT_LABEL, STOP_LABEL, PAUSE_LABEL, FOCUS_SESSION_ID, FOCUS_SESSION_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; const DEBUG_TOOLBAR_POSITION_KEY = 'debug.actionswidgetposition'; const DEBUG_TOOLBAR_Y_KEY = 'debug.actionswidgety'; @@ -75,15 +77,10 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { this.actionBar = this._register(new ActionBar(actionBarContainer, { orientation: ActionsOrientation.HORIZONTAL, actionViewItemProvider: (action: IAction) => { - if (action.id === FocusSessionAction.ID) { + if (action.id === FOCUS_SESSION_ID) { return this.instantiationService.createInstance(FocusSessionActionViewItem, action, undefined); - } else if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action); - } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); } - - return undefined; + return createActionViewItem(this.instantiationService, action); } })); @@ -94,7 +91,8 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { return this.hide(); } - const { actions, disposable } = DebugToolBar.getActions(this.debugToolBarMenu, this.debugService, this.instantiationService); + const actions: IAction[] = []; + const disposable = createAndFillInActionBarActions(this.debugToolBarMenu, { shouldForwardArgs: true }, actions, () => false); if (!arrays.equals(actions, this.activeActions, (first, second) => first.id === second.id && first.enabled === second.enabled)) { this.actionBar.clear(); this.actionBar.push(actions, { icon: true, label: false }); @@ -115,9 +113,11 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { private registerListeners(): void { this._register(this.debugService.onDidChangeState(() => this.updateScheduler.schedule())); - this._register(this.debugService.getViewModel().onDidFocusSession(() => this.updateScheduler.schedule())); - this._register(this.debugService.onDidNewSession(() => this.updateScheduler.schedule())); - this._register(this.configurationService.onDidChangeConfiguration(e => this.onDidConfigurationChange(e))); + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('debug.toolBarLocation')) { + this.updateScheduler.schedule(); + } + })); this._register(this.debugToolBarMenu.onDidChange(() => this.updateScheduler.schedule())); this._register(this.actionBar.actionRunner.onDidRun((e: IRunEvent) => { // check for error @@ -225,12 +225,6 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { } } - private onDidConfigurationChange(event: IConfigurationChangeEvent): void { - if (event.affectsConfiguration('debug.hideActionBar') || event.affectsConfiguration('debug.toolBarLocation')) { - this.updateScheduler.schedule(); - } - } - private show(): void { if (this.isVisible) { this.setCoordinates(); @@ -251,19 +245,6 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { dom.hide(this.$el); } - 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()) { - actions.push(instantiationService.createInstance(FocusSessionAction, FocusSessionAction.ID, FocusSessionAction.LABEL)); - } - - return { - actions: actions.filter(a => !(a instanceof Separator)), // do not render separators for now - disposable - }; - } - dispose(): void { super.dispose(); @@ -276,127 +257,43 @@ 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.")); +// Debug toolbar -export const debugToolBarBorder = registerColor('debugToolBar.border', { - dark: null, - light: null, - hc: null -}, localize('debugToolBarBorder', "Debug toolbar border color.")); +const registerDebugToolBarItem = (id: string, title: string, order: number, icon?: { light?: URI, dark?: URI } | ThemeIcon, when?: ContextKeyExpression, precondition?: ContextKeyExpression) => { + MenuRegistry.appendMenuItem(MenuId.DebugToolBar, { + group: 'navigation', + when, + order, + command: { + id, + title, + icon, + precondition + } + }); -export const debugIconStartForeground = registerColor('debugIcon.startForeground', { - dark: '#89D185', - light: '#388A34', - hc: '#89D185' -}, localize('debugIcon.startForeground', "Debug toolbar icon for start debugging.")); + // Register actions in debug viewlet when toolbar is docked + MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { + group: 'navigation', + when: ContextKeyExpr.and(when, ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), CONTEXT_DEBUG_STATE.notEqualsTo('inactive'), ContextKeyExpr.equals('config.debug.toolBarLocation', 'docked')), + order, + command: { + id, + title, + icon, + precondition + } + }); +}; -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); - if (debugIconStartColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStart)} { color: ${debugIconStartColor} !important; }`); - } - - const debugIconPauseColor = theme.getColor(debugIconPauseForeground); - if (debugIconPauseColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugPause)} { color: ${debugIconPauseColor} !important; }`); - } - - const debugIconStopColor = theme.getColor(debugIconStopForeground); - if (debugIconStopColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStop)} { color: ${debugIconStopColor} !important; }`); - } - - const debugIconDisconnectColor = theme.getColor(debugIconDisconnectForeground); - if (debugIconDisconnectColor) { - collector.addRule(`.monaco-workbench .debug-view-content ${ThemeIcon.asCSSSelector(icons.debugDisconnect)}, .monaco-workbench .debug-toolbar ${ThemeIcon.asCSSSelector(icons.debugDisconnect)} { color: ${debugIconDisconnectColor} !important; }`); - } - - const debugIconRestartColor = theme.getColor(debugIconRestartForeground); - if (debugIconRestartColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugRestart)}, .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugRestartFrame)} { color: ${debugIconRestartColor} !important; }`); - } - - const debugIconStepOverColor = theme.getColor(debugIconStepOverForeground); - if (debugIconStepOverColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepOver)} { color: ${debugIconStepOverColor} !important; }`); - } - - const debugIconStepIntoColor = theme.getColor(debugIconStepIntoForeground); - if (debugIconStepIntoColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepInto)} { color: ${debugIconStepIntoColor} !important; }`); - } - - const debugIconStepOutColor = theme.getColor(debugIconStepOutForeground); - if (debugIconStepOutColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepOut)} { color: ${debugIconStepOutColor} !important; }`); - } - - const debugIconContinueColor = theme.getColor(debugIconContinueForeground); - if (debugIconContinueColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugContinue)}, .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugReverseContinue)} { color: ${debugIconContinueColor} !important; }`); - } - - const debugIconStepBackColor = theme.getColor(debugIconStepBackForeground); - if (debugIconStepBackColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepBack)} { color: ${debugIconStepBackColor} !important; }`); - } -}); +registerDebugToolBarItem(CONTINUE_ID, CONTINUE_LABEL, 10, icons.debugContinue, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(PAUSE_ID, PAUSE_LABEL, 10, icons.debugPause, CONTEXT_DEBUG_STATE.notEqualsTo('stopped'), CONTEXT_DEBUG_STATE.isEqualTo('running')); +registerDebugToolBarItem(STOP_ID, STOP_LABEL, 70, icons.debugStop, CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated()); +registerDebugToolBarItem(DISCONNECT_ID, DISCONNECT_LABEL, 70, icons.debugDisconnect, CONTEXT_FOCUSED_SESSION_IS_ATTACH); +registerDebugToolBarItem(STEP_OVER_ID, STEP_OVER_LABEL, 20, icons.debugStepOver, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(STEP_INTO_ID, STEP_INTO_LABEL, 30, icons.debugStepInto, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(STEP_OUT_ID, STEP_OUT_LABEL, 40, icons.debugStepOut, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(RESTART_SESSION_ID, RESTART_LABEL, 60, icons.debugRestart); +registerDebugToolBarItem(STEP_BACK_ID, localize('stepBackDebug', "Step Back"), 50, icons.debugStepBack, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(REVERSE_CONTINUE_ID, localize('reverseContinue', "Reverse"), 60, icons.debugReverseContinue, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(FOCUS_SESSION_ID, FOCUS_SESSION_LABEL, 100, undefined, CONTEXT_MULTI_SESSION_DEBUG); diff --git a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts index 91187dd3..d0749ded 100644 --- a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts @@ -6,34 +6,36 @@ import 'vs/css!./media/debugViewlet'; import * as nls from 'vs/nls'; import { IAction, IActionViewItem } from 'vs/base/common/actions'; -import { IDebugService, VIEWLET_ID, State, BREAKPOINTS_VIEW_ID, IDebugConfiguration, CONTEXT_DEBUG_UX, CONTEXT_DEBUG_UX_KEY, REPL_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; -import { StartAction, ConfigureAction, SelectAndStartAction, FocusSessionAction } from 'vs/workbench/contrib/debug/browser/debugActions'; +import { IDebugService, VIEWLET_ID, State, BREAKPOINTS_VIEW_ID, CONTEXT_DEBUG_UX, CONTEXT_DEBUG_UX_KEY, REPL_VIEW_ID, CONTEXT_DEBUG_STATE, ILaunch, getStateLabel, CONTEXT_DEBUGGERS_AVAILABLE } from 'vs/workbench/contrib/debug/common/debug'; import { StartDebugActionViewItem, FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IProgressService } from 'vs/platform/progress/common/progress'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { memoize } from 'vs/base/common/decorators'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { DebugToolBar } from 'vs/workbench/contrib/debug/browser/debugToolBar'; -import { ViewPane, ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { IMenu, MenuId, IMenuService, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { ViewPaneContainer, ViewsSubMenu } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; +import { MenuId, registerAction2, Action2, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { IContextKeyService, ContextKeyEqualsExpr, ContextKeyExpr, ContextKeyDefinedExpr } from 'vs/platform/contextkey/common/contextkey'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IViewDescriptorService, IViewsService } from 'vs/workbench/common/views'; import { WelcomeView } from 'vs/workbench/contrib/debug/browser/welcomeView'; -import { ToggleViewAction } from 'vs/workbench/browser/actions/layoutActions'; -import { RunOnceScheduler } from 'vs/base/common/async'; import { ShowViewletAction } from 'vs/workbench/browser/viewlet'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { debugConsole } from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { debugConfigure, debugConsole } from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { ToggleViewAction } from 'vs/workbench/browser/actions/layoutActions'; +import { FOCUS_SESSION_ID, SELECT_AND_START_ID, DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL, DEBUG_START_LABEL, DEBUG_START_COMMAND_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; export class DebugViewPaneContainer extends ViewPaneContainer { @@ -41,9 +43,6 @@ export class DebugViewPaneContainer extends ViewPaneContainer { private progressResolve: (() => void) | undefined; private breakpointView: ViewPane | undefined; private paneListeners = new Map(); - private debugToolBarMenu: IMenu | undefined; - private disposeOnTitleUpdate: IDisposable | undefined; - private updateToolBarScheduler: RunOnceScheduler; constructor( @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @@ -58,22 +57,13 @@ export class DebugViewPaneContainer extends ViewPaneContainer { @IExtensionService extensionService: IExtensionService, @IConfigurationService configurationService: IConfigurationService, @IContextViewService private readonly contextViewService: IContextViewService, - @IMenuService private readonly menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService ) { super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); - this.updateToolBarScheduler = this._register(new RunOnceScheduler(() => { - if (this.configurationService.getValue('debug').toolBarLocation === 'docked') { - this.updateTitleArea(); - } - }, 20)); - // When there are potential updates to the docked debug toolbar we need to update it this._register(this.debugService.onDidChangeState(state => this.onDebugServiceStateChange(state))); - this._register(this.debugService.onDidNewSession(() => this.updateToolBarScheduler.schedule())); - this._register(this.debugService.getViewModel().onDidFocusSession(() => this.updateToolBarScheduler.schedule())); this._register(this.contextKeyService.onDidChangeContext(e => { if (e.affectsSome(new Set([CONTEXT_DEBUG_UX_KEY]))) { @@ -104,83 +94,15 @@ export class DebugViewPaneContainer extends ViewPaneContainer { } } - @memoize - private get startAction(): StartAction { - return this._register(this.instantiationService.createInstance(StartAction, StartAction.ID, StartAction.LABEL)); - } - - @memoize - private get configureAction(): ConfigureAction { - return this._register(this.instantiationService.createInstance(ConfigureAction, ConfigureAction.ID, ConfigureAction.LABEL)); - } - - @memoize - private get toggleReplAction(): OpenDebugConsoleAction { - return this._register(this.instantiationService.createInstance(OpenDebugConsoleAction, OpenDebugConsoleAction.ID, OpenDebugConsoleAction.LABEL)); - } - - @memoize - private get selectAndStartAction(): SelectAndStartAction { - return this._register(this.instantiationService.createInstance(SelectAndStartAction, SelectAndStartAction.ID, nls.localize('startAdditionalSession', "Start Additional Session"))); - } - - getActions(): IAction[] { - if (CONTEXT_DEBUG_UX.getValue(this.contextKeyService) === 'simple') { - return []; - } - - if (!this.showInitialDebugActions) { - - if (!this.debugToolBarMenu) { - this.debugToolBarMenu = this.menuService.createMenu(MenuId.DebugToolBar, this.contextKeyService); - this._register(this.debugToolBarMenu); - this._register(this.debugToolBarMenu.onDidChange(() => this.updateToolBarScheduler.schedule())); - } - - const { actions, disposable } = DebugToolBar.getActions(this.debugToolBarMenu, this.debugService, this.instantiationService); - if (this.disposeOnTitleUpdate) { - dispose(this.disposeOnTitleUpdate); - } - this.disposeOnTitleUpdate = disposable; - - return actions; - } - - if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { - return [this.toggleReplAction]; - } - - return [this.startAction, this.configureAction, this.toggleReplAction]; - } - - get showInitialDebugActions(): boolean { - const state = this.debugService.state; - return state === State.Inactive || this.configurationService.getValue('debug').toolBarLocation !== 'docked'; - } - - getSecondaryActions(): IAction[] { - if (this.showInitialDebugActions) { - return []; - } - - return [this.selectAndStartAction, this.configureAction, this.toggleReplAction]; - } - getActionViewItem(action: IAction): IActionViewItem | undefined { - if (action.id === StartAction.ID) { + if (action.id === DEBUG_START_COMMAND_ID) { this.startDebugActionViewItem = this.instantiationService.createInstance(StartDebugActionViewItem, null, action); return this.startDebugActionViewItem; } - if (action.id === FocusSessionAction.ID) { + if (action.id === FOCUS_SESSION_ID) { return new FocusSessionActionViewItem(action, undefined, this.debugService, this.themeService, this.contextViewService, this.configurationService); } - if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action); - } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); - } - - return undefined; + return createActionViewItem(this.instantiationService, action); } focusView(id: string): void { @@ -201,8 +123,6 @@ export class DebugViewPaneContainer extends ViewPaneContainer { return new Promise(resolve => this.progressResolve = resolve); }); } - - this.updateToolBarScheduler.schedule(); } addPanes(panes: { pane: ViewPane, size: number, index?: number }[]): void { @@ -236,22 +156,6 @@ export class DebugViewPaneContainer extends ViewPaneContainer { } } -export class OpenDebugConsoleAction extends ToggleViewAction { - public static readonly ID = 'workbench.debug.action.toggleRepl'; - public static readonly LABEL = nls.localize('toggleDebugPanel', "Debug Console"); - - constructor( - id: string, - label: string, - @IViewsService viewsService: IViewsService, - @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IContextKeyService contextKeyService: IContextKeyService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService - ) { - super(id, label, REPL_VIEW_ID, viewsService, viewDescriptorService, contextKeyService, layoutService, ThemeIcon.asClassName(debugConsole)); - } -} - export class OpenDebugViewletAction extends ShowViewletAction { public static readonly ID = VIEWLET_ID; public static readonly LABEL = nls.localize('toggleDebugViewlet', "Show Run and Debug"); @@ -266,3 +170,139 @@ export class OpenDebugViewletAction extends ShowViewletAction { super(id, label, VIEWLET_ID, viewletService, editorGroupService, layoutService); } } + +MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), CONTEXT_DEBUG_UX.notEqualsTo('simple'), WorkbenchStateContext.notEqualsTo('empty'), + ContextKeyExpr.or(CONTEXT_DEBUG_STATE.isEqualTo('inactive'), ContextKeyExpr.notEquals('config.debug.toolBarLocation', 'docked'))), + order: 10, + group: 'navigation', + command: { + precondition: CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing)), + id: DEBUG_START_COMMAND_ID, + title: DEBUG_START_LABEL + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: DEBUG_CONFIGURE_COMMAND_ID, + title: { + value: DEBUG_CONFIGURE_LABEL, + original: DEBUG_CONFIGURE_LABEL, + mnemonicTitle: nls.localize({ key: 'miOpenConfigurations', comment: ['&& denotes a mnemonic'] }, "Open &&Configurations") + }, + f1: true, + icon: debugConfigure, + precondition: CONTEXT_DEBUG_UX.notEqualsTo('simple'), + menu: [{ + id: MenuId.ViewContainerTitle, + group: 'navigation', + order: 20, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), CONTEXT_DEBUG_UX.notEqualsTo('simple'), WorkbenchStateContext.notEqualsTo('empty'), + ContextKeyExpr.or(CONTEXT_DEBUG_STATE.isEqualTo('inactive'), ContextKeyExpr.notEquals('config.debug.toolBarLocation', 'docked'))) + }, { + id: MenuId.ViewContainerTitle, + order: 20, + // Show in debug viewlet secondary actions when debugging and debug toolbar is docked + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), CONTEXT_DEBUG_STATE.notEqualsTo('inactive'), ContextKeyExpr.equals('config.debug.toolBarLocation', 'docked')) + }, { + id: MenuId.MenubarDebugMenu, + group: '2_configuration', + order: 1, + when: CONTEXT_DEBUGGERS_AVAILABLE + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + const debugService = accessor.get(IDebugService); + const quickInputService = accessor.get(IQuickInputService); + const configurationManager = debugService.getConfigurationManager(); + let launch: ILaunch | undefined; + if (configurationManager.selectedConfiguration.name) { + launch = configurationManager.selectedConfiguration.launch; + } else { + const launches = configurationManager.getLaunches().filter(l => !l.hidden); + if (launches.length === 1) { + launch = launches[0]; + } else { + const picks = launches.map(l => ({ label: l.name, launch: l })); + const picked = await quickInputService.pick<{ label: string, launch: ILaunch }>(picks, { + activeItem: picks[0], + placeHolder: nls.localize({ key: 'selectWorkspaceFolder', comment: ['User picks a workspace folder or a workspace configuration file here. Workspace configuration files can contain settings and thus a launch.json configuration can be written into one.'] }, "Select a workspace folder to create a launch.json file in or add it to the workspace config file") + }); + if (picked) { + launch = picked.launch; + } + } + } + + if (launch) { + await launch.openConfigFile(false); + } + } +}); + +export const OPEN_REPL_COMMAND_ID = 'workbench.debug.action.toggleRepl'; +registerAction2(class extends Action2 { + constructor() { + super({ + id: OPEN_REPL_COMMAND_ID, + title: { + value: nls.localize('debugPanel', "Debug Console"), + original: 'Debug Console', + mnemonicTitle: nls.localize({ key: 'miToggleDebugConsole', comment: ['&& denotes a mnemonic'] }, "De&&bug Console") + }, + f1: true, + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Y, + weight: KeybindingWeight.WorkbenchContrib + }, + icon: debugConsole, + menu: [{ + id: MenuId.MenubarViewMenu, + group: '4_panels', + order: 2 + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + return accessor.get(IInstantiationService).createInstance(ToggleViewAction, OPEN_REPL_COMMAND_ID, 'Debug Console', REPL_VIEW_ID).run(); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'debug.toggleReplIgnoreFocus', + title: nls.localize('debugPanel', "Debug Console"), + toggled: ContextKeyDefinedExpr.create(`view.${REPL_VIEW_ID}.visible`), + menu: [{ + id: ViewsSubMenu, + group: '3_toggleRepl', + order: 30, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID)) + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + const viewsService = accessor.get(IViewsService); + if (viewsService.isViewVisible(REPL_VIEW_ID)) { + viewsService.closeView(REPL_VIEW_ID); + } else { + await viewsService.openView(REPL_VIEW_ID); + } + } +}); + +MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), CONTEXT_DEBUG_STATE.notEqualsTo('inactive'), ContextKeyExpr.equals('config.debug.toolBarLocation', 'docked')), + order: 10, + command: { + id: SELECT_AND_START_ID, + title: nls.localize('startAdditionalSession', "Start Additional Session"), + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/linkDetector.ts b/src/vs/workbench/contrib/debug/browser/linkDetector.ts index e927b779..14ea8896 100644 --- a/src/vs/workbench/contrib/debug/browser/linkDetector.ts +++ b/src/vs/workbench/contrib/debug/browser/linkDetector.ts @@ -3,16 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Schemas } from 'vs/base/common/network'; import * as osPath from 'vs/base/common/path'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; -import * as nls from 'vs/nls'; import { IFileService } from 'vs/platform/files/common/files'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; const CONTROL_CODES = '\\u0000-\\u0020\\u007f-\\u009f'; const WEB_LINK_REGEX = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|data:|www\\.)[^\\s' + CONTROL_CODES + '"]{2,}[^\\s' + CONTROL_CODES + '"\')}\\],:;.!?]', 'ug'); @@ -97,8 +99,28 @@ export class LinkDetector { private createWebLink(url: string): Node { const link = this.createLink(url); - const uri = URI.parse(url); - this.decorateLink(link, () => this.openerService.open(uri, { allowTunneling: !!this.environmentService.remoteAuthority })); + + this.decorateLink(link, async () => { + const uri = URI.parse(url); + + if (uri.scheme === Schemas.file) { + // Just using fsPath here is unsafe: https://github.com/microsoft/vscode/issues/109076 + const fsPath = uri.fsPath; + const path = await this.pathService.path; + const fileUrl = osPath.normalize(((path.sep === osPath.posix.sep) && platform.isWindows) ? fsPath.replace(/\\/g, osPath.posix.sep) : fsPath); + + const resolvedLink = await this.fileService.resolve(URI.parse(fileUrl)); + if (!resolvedLink) { + return; + } + + await this.editorService.openEditor({ resource: resolvedLink.resource, options: { pinned: true } }); + return; + } + + this.openerService.open(url, { allowTunneling: !!this.environmentService.remoteAuthority }); + }); + return link; } @@ -108,14 +130,14 @@ export class LinkDetector { return document.createTextNode(text); } + const options = { selection: { startLineNumber: lineNumber, startColumn: columnNumber } }; if (path[0] === '.') { if (!workspaceFolder) { return document.createTextNode(text); } const uri = workspaceFolder.toResource(path); - const options = { selection: { startLineNumber: lineNumber, startColumn: columnNumber } }; const link = this.createLink(text); - this.decorateLink(link, () => this.editorService.openEditor({ resource: uri, options })); + this.decorateLink(link, (preserveFocus: boolean) => this.editorService.openEditor({ resource: uri, options: { ...options, preserveFocus } })); return link; } @@ -127,13 +149,13 @@ export class LinkDetector { } const link = this.createLink(text); + link.tabIndex = 0; const uri = URI.file(osPath.normalize(path)); this.fileService.resolve(uri).then(stat => { if (stat.isDirectory) { return; } - const options = { selection: { startLineNumber: lineNumber, startColumn: columnNumber } }; - this.decorateLink(link, () => this.editorService.openEditor({ resource: uri, options })); + this.decorateLink(link, (preserveFocus: boolean) => this.editorService.openEditor({ resource: uri, options: { ...options, preserveFocus } })); }).catch(() => { // If the uri can not be resolved we should not spam the console with error, remain quite #86587 }); @@ -146,9 +168,8 @@ export class LinkDetector { return link; } - private decorateLink(link: HTMLElement, onclick: () => void) { + private decorateLink(link: HTMLElement, onClick: (preserveFocus: boolean) => void) { link.classList.add('link'); - link.title = platform.isMacintosh ? nls.localize('fileLinkMac', "Cmd + click to follow link") : nls.localize('fileLink', "Ctrl + click to follow link"); link.onmousemove = (event) => { link.classList.toggle('pointer', platform.isMacintosh ? event.metaKey : event.ctrlKey); }; link.onmouseleave = () => link.classList.remove('pointer'); link.onclick = (event) => { @@ -156,12 +177,17 @@ export class LinkDetector { if (!selection || selection.type === 'Range') { return; // do not navigate when user is selecting } - if (!(platform.isMacintosh ? event.metaKey : event.ctrlKey)) { - return; - } event.preventDefault(); event.stopImmediatePropagation(); - onclick(); + onClick(false); + }; + link.onkeydown = e => { + const event = new StandardKeyboardEvent(e); + if (event.keyCode === KeyCode.Enter || event.keyCode === KeyCode.Space) { + event.preventDefault(); + event.stopPropagation(); + onClick(event.keyCode === KeyCode.Space); + } }; } diff --git a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts index 198da910..6ebf580c 100644 --- a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts +++ b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { normalize, isAbsolute, posix } from 'vs/base/common/path'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index 47cafc6a..5487c7f7 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -144,22 +144,22 @@ display: none; } -.debug-pane .debug-call-stack .monaco-list-row .monaco-action-bar { +.debug-pane .monaco-list-row .monaco-action-bar { display: none; flex-shrink: 0; margin-right: 1px; } -.debug-pane .debug-call-stack .monaco-list-row:hover .monaco-action-bar { +.debug-pane .monaco-list-row:hover .monaco-action-bar { display: initial; } -.debug-pane .debug-call-stack .session .codicon { +.debug-pane .session .codicon { line-height: 22px; margin-right: 2px; } -.monaco-workbench .debug-pane .debug-call-stack .monaco-action-bar .action-item > .action-label { +.monaco-workbench .debug-pane .monaco-action-bar .action-item > .action-label { width: 16px; height: 100%; line-height: 22px; @@ -259,11 +259,11 @@ font-family: var(--monaco-monospace-font); } -.debug-pane .monaco-inputbox > .wrapper { +.debug-pane .monaco-inputbox > .ibwrapper { height: 19px; } -.debug-pane .monaco-inputbox > .wrapper > .input { +.debug-pane .monaco-inputbox > .ibwrapper > .input { padding: 0px; color: initial; } @@ -319,7 +319,7 @@ } .debug-pane .debug-breakpoints .breakpoint > .file-path, -.debug-pane .debug-breakpoints .breakpoint.exception > .condition { +.debug-pane .debug-breakpoints .breakpoint > .condition { opacity: 0.7; margin-left: 0.9em; flex: 1; diff --git a/src/vs/workbench/contrib/debug/browser/media/repl.css b/src/vs/workbench/contrib/debug/browser/media/repl.css index 04de3a21..d48c7b77 100644 --- a/src/vs/workbench/contrib/debug/browser/media/repl.css +++ b/src/vs/workbench/contrib/debug/browser/media/repl.css @@ -17,6 +17,10 @@ white-space: pre; } +.monaco-workbench .repl .repl-tree .monaco-tl-contents .expression { + font-family: var(--vscode-repl-font-family); +} + .monaco-workbench .repl .repl-tree.word-wrap .monaco-tl-contents { /* Wrap words but also do not trim whitespace #6275 */ word-wrap: break-word; diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index a7003659..19e9bc3b 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -154,6 +154,10 @@ export class RawDebugSession implements IDisposable { case 'invalidated': this._onDidInvalidated.fire(event as DebugProtocol.InvalidatedEvent); break; + case 'process': + break; + case 'module': + break; default: this._onDidCustomEvent.fire(event); break; diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 74467fa8..1afbbaf1 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/repl'; import { URI as uri } from 'vs/base/common/uri'; -import { IAction, IActionViewItem, Action, Separator } from 'vs/base/common/actions'; +import { IAction, IActionViewItem } from 'vs/base/common/actions'; import * as dom from 'vs/base/browser/dom'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -14,11 +14,11 @@ 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 { registerEditorAction, 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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IContextKeyService, IContextKey, ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -26,7 +26,7 @@ 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, DEBUG_SCHEME, CONTEXT_IN_DEBUG_REPL, IDebugSession, State, IReplElement, IDebugConfiguration, REPL_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, DEBUG_SCHEME, CONTEXT_IN_DEBUG_REPL, IDebugSession, State, IReplElement, IDebugConfiguration, REPL_VIEW_ID, CONTEXT_MULTI_SESSION_REPL, CONTEXT_DEBUG_STATE, getStateLabel } 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'; @@ -50,7 +50,7 @@ import { FuzzyScore } from 'vs/base/common/filters'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ReplDelegate, ReplVariablesRenderer, ReplSimpleElementsRenderer, ReplEvaluationInputsRenderer, ReplEvaluationResultsRenderer, ReplRawObjectsRenderer, ReplDataSource, ReplAccessibilityProvider, ReplGroupRenderer } from 'vs/workbench/contrib/debug/browser/replViewer'; import { localize } from 'vs/nls'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IViewsService, IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -60,6 +60,8 @@ import { EDITOR_FONT_DEFAULTS, EditorOption } from 'vs/editor/common/config/edit import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; import { ReplFilter, ReplFilterState, ReplFilterActionViewItem } from 'vs/workbench/contrib/debug/browser/replFilter'; import { debugConsoleClearAll, debugConsoleEvaluationPrompt } from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { registerAction2, MenuId, Action2, IMenuService, IMenu } from 'vs/platform/actions/common/actions'; +import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; const $ = dom.$; @@ -73,17 +75,19 @@ function revealLastElement(tree: WorkbenchAsyncDataTree) { } const sessionsToIgnore = new Set(); +const identityProvider = { getId: (element: IReplElement) => element.getId() }; export class Repl extends ViewPane implements IHistoryNavigationWidget { declare readonly _serviceBrand: undefined; - private static readonly REFRESH_DELAY = 100; // delay in ms to refresh the repl for new elements to show + private static readonly REFRESH_DELAY = 50; // delay in ms to refresh the repl for new elements to show private static readonly URI = uri.parse(`${DEBUG_SCHEME}:replinput`); private history: HistoryNavigator; private tree!: WorkbenchAsyncDataTree; private replDelegate!: ReplDelegate; private container!: HTMLElement; + private treeContainer!: HTMLElement; private replInput!: CodeEditorWidget; private replInputContainer!: HTMLElement; private dimension!: dom.Dimension; @@ -98,6 +102,8 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { private filter: ReplFilter; private filterState: ReplFilterState; private filterActionViewItem: ReplFilterActionViewItem | undefined; + private multiSessionRepl: IContextKey; + private menu: IMenu; constructor( options: IViewPaneOptions, @@ -112,17 +118,21 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, @ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService, - @IClipboardService private readonly clipboardService: IClipboardService, @IEditorService private readonly editorService: IEditorService, @IKeybindingService keybindingService: IKeybindingService, @IOpenerService openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService, + @IMenuService menuService: IMenuService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + this.menu = menuService.createMenu(MenuId.DebugConsoleContext, contextKeyService); + this._register(this.menu); this.history = new HistoryNavigator(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')), 50); this.filter = new ReplFilter(); this.filterState = new ReplFilterState(this); + this.multiSessionRepl = CONTEXT_MULTI_SESSION_REPL.bindTo(contextKeyService); + this.multiSessionRepl.set(this.isMultiSessionView); codeEditorService.registerDecorationType(DECORATION_KEY, {}); this.registerListeners(); @@ -207,7 +217,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { if (!input || input.state === State.Inactive) { await this.selectSession(newSession); } - this.updateActions(); + this.multiSessionRepl.set(this.isMultiSessionView); })); this._register(this.themeService.onDidColorThemeChange(() => { this.refreshReplElements(false); @@ -224,10 +234,16 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.replInput.setModel(this.model); this.updateInputDecoration(); this.refreshReplElements(true); + this.layoutBody(this.dimension.height, this.dimension.width); } })); this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('debug.console.lineHeight') || e.affectsConfiguration('debug.console.fontSize') || e.affectsConfiguration('debug.console.fontFamily')) { + if (e.affectsConfiguration('debug.console.wordWrap')) { + this.tree.dispose(); + this.treeContainer.innerText = ''; + dom.clearNode(this.treeContainer); + this.createReplTree(); + } else if (e.affectsConfiguration('debug.console.lineHeight') || e.affectsConfiguration('debug.console.fontSize') || e.affectsConfiguration('debug.console.fontFamily')) { this.onDidStyleChange(); } })); @@ -305,7 +321,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { if (this.styleElement) { const debugConsole = this.configurationService.getValue('debug').console; const fontSize = debugConsole.fontSize; - const fontFamily = debugConsole.fontFamily === 'default' ? 'var(--monaco-monospace-font)' : `'${debugConsole.fontFamily}'`; + const fontFamily = debugConsole.fontFamily === 'default' ? 'var(--monaco-monospace-font)' : `${debugConsole.fontFamily}`; const lineHeight = debugConsole.lineHeight ? `${debugConsole.lineHeight}px` : '1.4em'; const backgroundColor = this.themeService.getColorTheme().getColor(this.getBackgroundColor()); @@ -321,7 +337,6 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.styleElement.textContent = ` .repl .repl-tree .expression { font-size: ${fontSize}px; - font-family: ${fontFamily}; } .repl .repl-tree .expression { @@ -340,6 +355,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { background-color: ${backgroundColor}; } `; + this.container.style.setProperty(`--vscode-repl-font-family`, fontFamily); this.tree.rerender(); @@ -397,7 +413,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { // Ignore inactive sessions which got cleared - so they are not shown any more sessionsToIgnore.add(session); await this.selectSession(); - this.updateActions(); + this.multiSessionRepl.set(this.isMultiSessionView); } } this.replInput.focus(); @@ -455,14 +471,22 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.replInput.layout({ width: width - 30, height: replInputHeight }); } + collapseAll(): void { + this.tree.collapseAll(); + } + + getReplInput(): CodeEditorWidget { + return this.replInput; + } + focus(): void { setTimeout(() => this.replInput.focus(), 0); } getActionViewItem(action: IAction): IActionViewItem | undefined { - if (action.id === SelectReplAction.ID) { + if (action.id === selectReplCommandId) { const session = (this.tree ? this.tree.getInput() : undefined) ?? this.debugService.getViewModel().focusedSession; - return this.instantiationService.createInstance(SelectReplActionViewItem, this.selectReplAction, session); + return this.instantiationService.createInstance(SelectReplActionViewItem, action, session); } else if (action.id === FILTER_ACTION_ID) { const filterHistory = JSON.parse(this.storageService.get(FILTER_HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')) as string[]; this.filterActionViewItem = this.instantiationService.createInstance(ReplFilterActionViewItem, action, @@ -473,29 +497,11 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { return super.getActionViewItem(action); } - getActions(): IAction[] { - const result: IAction[] = []; - result.push(new Action(FILTER_ACTION_ID)); - if (this.debugService.getModel().getSessions(true).filter(s => s.hasSeparateRepl() && !sessionsToIgnore.has(s)).length > 1) { - result.push(this.selectReplAction); - } - result.push(this.clearReplAction); - - result.forEach(a => this._register(a)); - - return result; + private get isMultiSessionView(): boolean { + return this.debugService.getModel().getSessions(true).filter(s => s.hasSeparateRepl() && !sessionsToIgnore.has(s)).length > 1; } // --- Cached locals - @memoize - private get selectReplAction(): SelectReplAction { - return this.instantiationService.createInstance(SelectReplAction, SelectReplAction.ID, SelectReplAction.LABEL); - } - - @memoize - private get clearReplAction(): ClearReplAction { - return this.instantiationService.createInstance(ClearReplAction, ClearReplAction.ID, ClearReplAction.LABEL); - } @memoize private get refreshScheduler(): RunOnceScheduler { @@ -506,7 +512,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { } const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight; - await this.tree.updateChildren(); + await this.tree.updateChildren(undefined, true, false, { diffIdentityProvider: identityProvider }); const session = this.tree.getInput(); if (session) { @@ -541,25 +547,27 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { protected renderBody(parent: HTMLElement): void { super.renderBody(parent); - this.container = dom.append(parent, $('.repl')); - const treeContainer = dom.append(this.container, $(`.repl-tree.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`)); + this.treeContainer = dom.append(this.container, $(`.repl-tree.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`)); this.createReplInput(this.container); + this.createReplTree(); + } + private createReplTree(): void { this.replDelegate = new ReplDelegate(this.configurationService); const wordWrap = this.configurationService.getValue('debug').console.wordWrap; - treeContainer.classList.toggle('word-wrap', wordWrap); + this.treeContainer.classList.toggle('word-wrap', wordWrap); const linkDetector = this.instantiationService.createInstance(LinkDetector); this.tree = >this.instantiationService.createInstance( WorkbenchAsyncDataTree, 'DebugRepl', - treeContainer, + this.treeContainer, this.replDelegate, [ this.instantiationService.createInstance(ReplVariablesRenderer, linkDetector), this.instantiationService.createInstance(ReplSimpleElementsRenderer, linkDetector), new ReplEvaluationInputsRenderer(), - new ReplGroupRenderer(), + this.instantiationService.createInstance(ReplGroupRenderer, linkDetector), new ReplEvaluationResultsRenderer(linkDetector), new ReplRawObjectsRenderer(linkDetector), ], @@ -568,7 +576,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { { filter: this.filter, accessibilityProvider: new ReplAccessibilityProvider(), - identityProvider: { getId: (element: IReplElement) => element.getId() }, + identityProvider, mouseSupport: false, keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IReplElement) => e }, horizontalScrolling: !wordWrap, @@ -598,7 +606,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.replInputContainer = dom.append(container, $('.repl-input-wrapper')); dom.append(this.replInputContainer, $('.repl-input-chevron' + ThemeIcon.asCSSSelector(debugConsoleEvaluationPrompt))); - const { scopedContextKeyService, historyNavigationEnablement } = createAndBindHistoryNavigationWidgetScopedContextKeyService(this.contextKeyService, { target: this.replInputContainer, historyNavigator: this }); + const { scopedContextKeyService, historyNavigationEnablement } = createAndBindHistoryNavigationWidgetScopedContextKeyService(this.contextKeyService, { target: container, historyNavigator: this }); this.historyNavigationEnablement = historyNavigationEnablement; this._register(scopedContextKeyService); CONTEXT_IN_DEBUG_REPL.bindTo(scopedContextKeyService).set(true); @@ -629,44 +637,12 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { private onContextMenu(e: ITreeContextMenuEvent): void { const actions: IAction[] = []; - actions.push(new Action('debug.replCopy', localize('copy', "Copy"), undefined, true, async () => { - const nativeSelection = window.getSelection(); - if (nativeSelection) { - await this.clipboardService.writeText(nativeSelection.toString()); - } - return Promise.resolve(); - })); - actions.push(new Action('workbench.debug.action.copyAll', localize('copyAll', "Copy All"), undefined, true, async () => { - await this.clipboardService.writeText(this.getVisibleContent()); - return Promise.resolve(); - })); - actions.push(new Action('debug.replPaste', localize('paste', "Paste"), undefined, this.debugService.state !== State.Inactive, async () => { - const clipboardText = await this.clipboardService.readText(); - if (clipboardText) { - this.replInput.setValue(this.replInput.getValue().concat(clipboardText)); - this.replInput.focus(); - const model = this.replInput.getModel(); - const lineNumber = model ? model.getLineCount() : 0; - const column = model?.getLineMaxColumn(lineNumber); - if (typeof lineNumber === 'number' && typeof column === 'number') { - this.replInput.setPosition({ lineNumber, column }); - } - } - })); - actions.push(new Separator()); - actions.push(new Action('debug.collapseRepl', localize('collapse', "Collapse All"), undefined, true, () => { - this.tree.collapseAll(); - this.replInput.focus(); - return Promise.resolve(); - })); - actions.push(new Separator()); - actions.push(this.clearReplAction); - + const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: e.element, shouldForwardArgs: false }, actions); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, getActions: () => actions, getActionsContext: () => e.element, - onHide: () => dispose(actions) + onHide: () => dispose(actionsDisposable) }); } @@ -825,48 +801,183 @@ class SelectReplActionViewItem extends FocusSessionActionViewItem { } } -class SelectReplAction extends Action { - - static readonly ID = 'workbench.action.debug.selectRepl'; - static readonly LABEL = localize('selectRepl', "Select Debug Console"); - - constructor(id: string, label: string, - @IDebugService private readonly debugService: IDebugService, - @IViewsService private readonly viewsService: IViewsService - ) { - super(id, label); - } - - async run(session: IDebugSession): Promise { - // If session is already the focused session we need to manualy update the tree since view model will not send a focused change event - if (session && session.state !== State.Inactive && session !== this.debugService.getViewModel().focusedSession) { - await this.debugService.focusStackFrame(undefined, undefined, session, true); - } else { - const repl = getReplView(this.viewsService); - if (repl) { - await repl.selectSession(session); - } - } - } -} - -export class ClearReplAction extends Action { - static readonly ID = 'workbench.debug.panel.action.clearReplAction'; - static readonly LABEL = localize('clearRepl', "Clear Console"); - - constructor(id: string, label: string, - @IViewsService private readonly viewsService: IViewsService - ) { - super(id, label, 'debug-action ' + ThemeIcon.asClassName(debugConsoleClearAll)); - } - - async run(): Promise { - const view = await this.viewsService.openView(REPL_VIEW_ID) as Repl; - await view.clearRepl(); - aria.status(localize('debugConsoleCleared', "Debug console was cleared")); - } -} - function getReplView(viewsService: IViewsService): Repl | undefined { return viewsService.getActiveViewWithId(REPL_VIEW_ID) as Repl ?? undefined; } + +registerAction2(class extends Action2 { + constructor() { + super({ + id: FILTER_ACTION_ID, + title: localize('filter', "Filter"), + f1: true, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', REPL_VIEW_ID), + order: 10 + } + }); + } + + run(_accessor: ServicesAccessor) { + // noop this action is just a placeholder for the filter action view item + } +}); + +const selectReplCommandId = 'workbench.action.debug.selectRepl'; +registerAction2(class extends ViewAction { + constructor() { + super({ + id: selectReplCommandId, + viewId: REPL_VIEW_ID, + title: localize('selectRepl', "Select Debug Console"), + f1: true, + icon: debugConsoleClearAll, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', REPL_VIEW_ID), CONTEXT_MULTI_SESSION_REPL), + order: 20 + } + }); + } + + async runInView(accessor: ServicesAccessor, view: Repl, session: IDebugSession | undefined) { + const debugService = accessor.get(IDebugService); + // If session is already the focused session we need to manualy update the tree since view model will not send a focused change event + if (session && session.state !== State.Inactive && session !== debugService.getViewModel().focusedSession) { + if (session.state !== State.Stopped) { + // Focus child session instead if it is stopped #112595 + const stopppedChildSession = debugService.getModel().getSessions().find(s => s.parentSession === session); + if (stopppedChildSession) { + session = stopppedChildSession; + } + } + await debugService.focusStackFrame(undefined, undefined, session, true); + } else { + await view.selectSession(session); + } + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'workbench.debug.panel.action.clearReplAction', + viewId: REPL_VIEW_ID, + title: localize('clearRepl', "Clear Console"), + f1: true, + icon: debugConsoleClearAll, + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', REPL_VIEW_ID), + order: 30 + }, { + id: MenuId.DebugConsoleContext, + group: 'z_commands', + order: 20 + }] + }); + } + + runInView(_accessor: ServicesAccessor, view: Repl): void { + view.clearRepl(); + aria.status(localize('debugConsoleCleared', "Debug console was cleared")); + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'debug.collapseRepl', + title: localize('collapse', "Collapse All"), + viewId: REPL_VIEW_ID, + menu: { + id: MenuId.DebugConsoleContext, + group: 'z_commands', + order: 10 + } + }); + } + + runInView(_accessor: ServicesAccessor, view: Repl): void { + view.collapseAll(); + view.focus(); + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'debug.replPaste', + title: localize('paste', "Paste"), + viewId: REPL_VIEW_ID, + precondition: CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Inactive)), + menu: { + id: MenuId.DebugConsoleContext, + group: '2_cutcopypaste', + order: 30 + } + }); + } + + async runInView(accessor: ServicesAccessor, view: Repl): Promise { + const clipboardService = accessor.get(IClipboardService); + const clipboardText = await clipboardService.readText(); + if (clipboardText) { + const replInput = view.getReplInput(); + replInput.setValue(replInput.getValue().concat(clipboardText)); + view.focus(); + const model = replInput.getModel(); + const lineNumber = model ? model.getLineCount() : 0; + const column = model?.getLineMaxColumn(lineNumber); + if (typeof lineNumber === 'number' && typeof column === 'number') { + replInput.setPosition({ lineNumber, column }); + } + } + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'workbench.debug.action.copyAll', + title: localize('copyAll', "Copy All"), + viewId: REPL_VIEW_ID, + menu: { + id: MenuId.DebugConsoleContext, + group: '2_cutcopypaste', + order: 20 + } + }); + } + + async runInView(accessor: ServicesAccessor, view: Repl): Promise { + const clipboardService = accessor.get(IClipboardService); + await clipboardService.writeText(view.getVisibleContent()); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'debug.replCopy', + title: localize('copy', "Copy"), + menu: { + id: MenuId.DebugConsoleContext, + group: '2_cutcopypaste', + order: 10 + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const clipboardService = accessor.get(IClipboardService); + const nativeSelection = window.getSelection(); + if (nativeSelection) { + await clipboardService.writeText(nativeSelection.toString()); + } + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/replViewer.ts b/src/vs/workbench/contrib/debug/browser/replViewer.ts index 0d432417..937976c6 100644 --- a/src/vs/workbench/contrib/debug/browser/replViewer.ts +++ b/src/vs/workbench/contrib/debug/browser/replViewer.ts @@ -34,7 +34,7 @@ interface IReplEvaluationInputTemplateData { } interface IReplGroupTemplateData { - label: HighlightedLabel; + label: HTMLElement; } interface IReplEvaluationResultTemplateData { @@ -87,22 +87,28 @@ export class ReplEvaluationInputsRenderer implements ITreeRenderer { static readonly ID = 'replGroup'; + constructor( + private readonly linkDetector: LinkDetector, + @IThemeService private readonly themeService: IThemeService + ) { } + get templateId(): string { return ReplGroupRenderer.ID; } - renderTemplate(container: HTMLElement): IReplEvaluationInputTemplateData { - const input = dom.append(container, $('.expression')); - const label = new HighlightedLabel(input, false); + renderTemplate(container: HTMLElement): IReplGroupTemplateData { + const label = dom.append(container, $('.expression')); return { label }; } renderElement(element: ITreeNode, _index: number, templateData: IReplGroupTemplateData): void { const replGroup = element.element; - templateData.label.set(replGroup.name, createMatches(element.filterData)); + dom.clearNode(templateData.label); + const result = handleANSIOutput(replGroup.name, this.linkDetector, this.themeService, undefined); + templateData.label.appendChild(result); } - disposeTemplate(_templateData: IReplEvaluationInputTemplateData): void { + disposeTemplate(_templateData: IReplGroupTemplateData): void { // noop } } @@ -300,15 +306,15 @@ export class ReplDelegate extends CachedListVirtualDelegate { protected estimateHeight(element: IReplElement, ignoreValueLength = false): number { const config = this.configurationService.getValue('debug'); - const rowHeight = Math.ceil(1.4 * config.console.fontSize); + const rowHeight = Math.ceil(1.3 * config.console.fontSize); const countNumberOfLines = (str: string) => Math.max(1, (str && str.match(/\r\n|\n/g) || []).length); const hasValue = (e: any): e is { value: string } => typeof e.value === 'string'; // Calculate a rough overestimation for the height - // For every 30 characters increase the number of lines needed - if (hasValue(element)) { + // For every 70 characters increase the number of lines needed beyond the first + if (hasValue(element) && !(element instanceof Variable)) { let value = element.value; - let valueRows = countNumberOfLines(value) + (ignoreValueLength ? 0 : Math.floor(value.length / 30)); + let valueRows = countNumberOfLines(value) + (ignoreValueLength ? 0 : Math.floor(value.length / 70)); return valueRows * rowHeight; } @@ -338,6 +344,10 @@ export class ReplDelegate extends CachedListVirtualDelegate { } hasDynamicHeight(element: IReplElement): boolean { + if (element instanceof Variable) { + // Variables should always be in one line #111843 + return false; + } // Empty elements should not have dynamic height since they will be invisible return element.toString().length > 0; } @@ -383,7 +393,8 @@ export class ReplAccessibilityProvider implements IListAccessibilityProvider 1 ? localize('occurred', ", occured {0} times", element.count) : ''); + return element.value + (element instanceof SimpleReplElement && element.count > 1 ? localize({ key: 'occurred', comment: ['Front will the value of the debug console element. Placeholder will be replaced by a number which represents occurrance count.'] }, + ", occured {0} times", element.count) : ''); } if (element instanceof RawObjectReplElement) { return localize('replRawObjectAriaLabel', "Debug console variable {0}, value {1}", element.name, element.value); diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 7c8acdc4..665894a0 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -3,20 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { RunOnceScheduler } from 'vs/base/common/async'; import * as dom from 'vs/base/browser/dom'; -import { CollapseAction } from 'vs/workbench/browser/viewlet'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { IDebugService, IExpression, IScope, CONTEXT_VARIABLES_FOCUSED, IStackFrame, CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT, IDataBreakpointInfoResponse, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT } from 'vs/workbench/contrib/debug/common/debug'; -import { Variable, Scope, ErrorScope, StackFrame } from 'vs/workbench/contrib/debug/common/debugModel'; +import { IDebugService, IExpression, IScope, CONTEXT_VARIABLES_FOCUSED, IStackFrame, CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT, IDataBreakpointInfoResponse, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, VARIABLES_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { Variable, Scope, ErrorScope, StackFrame, Expression } from 'vs/workbench/contrib/debug/common/debugModel'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { renderViewTree, renderVariable, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { IAction } from 'vs/base/common/actions'; -import { CopyValueAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeRenderer, ITreeNode, ITreeMouseEvent, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; @@ -26,17 +23,19 @@ import { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKey, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { dispose } from 'vs/base/common/lifecycle'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { withUndefinedAsNull } from 'vs/base/common/types'; -import { IMenuService, IMenu, MenuId } from 'vs/platform/actions/common/actions'; +import { IMenuService, IMenu, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { debugCollapseAll } from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { localize } from 'vs/nls'; +import { Codicon } from 'vs/base/common/codicons'; +import { coalesce } from 'vs/base/common/arrays'; const $ = dom.$; let forgetScopes = true; @@ -179,10 +178,6 @@ export class VariablesView extends ViewPane { })); } - getActions(): IAction[] { - return [new CollapseAction(() => this.tree, true, 'explorer-action ' + ThemeIcon.asClassName(debugCollapseAll))]; - } - layoutBody(width: number, height: number): void { super.layoutBody(height, width); this.tree.layout(width, height); @@ -192,6 +187,10 @@ export class VariablesView extends ViewPane { this.tree.domFocus(); } + collapseAll(): void { + this.tree.collapseAll(); + } + private onMouseDblClick(e: ITreeMouseEvent): void { const session = this.debugService.getViewModel().focusedSession; if (session && e.element instanceof Variable && session.capabilities.supportsSetVariable) { @@ -345,7 +344,7 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { const variable = expression; return { initialValue: expression.value, - ariaLabel: nls.localize('variableValueAriaLabel', "Type new variable value"), + ariaLabel: localize('variableValueAriaLabel', "Type new variable value"), validationOptions: { validation: () => variable.errorMessage ? ({ content: variable.errorMessage }) : null }, @@ -368,15 +367,15 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { class VariablesAccessibilityProvider implements IListAccessibilityProvider { getWidgetAriaLabel(): string { - return nls.localize('variablesAriaTreeLabel', "Debug Variables"); + return localize('variablesAriaTreeLabel', "Debug Variables"); } getAriaLabel(element: IExpression | IScope): string | null { if (element instanceof Scope) { - return nls.localize('variableScopeAriaLabel', "Scope {0}", element.name); + return localize('variableScopeAriaLabel', "Scope {0}", element.name); } if (element instanceof Variable) { - return nls.localize({ key: 'variableAriaLabel', comment: ['Placeholders are variable name and variable value respectivly. They should not be translated.'] }, "{0}, value {1}", element.name, element.value); + return localize({ key: 'variableAriaLabel', comment: ['Placeholders are variable name and variable value respectivly. They should not be translated.'] }, "{0}, value {1}", element.name, element.value); } return null; @@ -392,14 +391,40 @@ CommandsRegistry.registerCommand({ } }); -export const COPY_VALUE_ID = 'debug.copyValue'; +export const COPY_VALUE_ID = 'workbench.debug.viewlet.action.copyValue'; CommandsRegistry.registerCommand({ id: COPY_VALUE_ID, - handler: async (accessor: ServicesAccessor) => { - const instantiationService = accessor.get(IInstantiationService); - if (variableInternalContext) { - const action = instantiationService.createInstance(CopyValueAction, CopyValueAction.ID, CopyValueAction.LABEL, variableInternalContext, 'variables'); - await action.run(); + handler: async (accessor: ServicesAccessor, arg: Variable | Expression | unknown, ctx?: (Variable | Expression)[]) => { + const debugService = accessor.get(IDebugService); + const clipboardService = accessor.get(IClipboardService); + let elementContext = ''; + let elements: (Variable | Expression)[]; + if (arg instanceof Variable || arg instanceof Expression) { + elementContext = 'watch'; + elements = ctx ? ctx : []; + } else { + elementContext = 'variables'; + elements = variableInternalContext ? [variableInternalContext] : []; + } + + const stackFrame = debugService.getViewModel().focusedStackFrame; + const session = debugService.getViewModel().focusedSession; + if (!stackFrame || !session || elements.length === 0) { + return; + } + + const evalContext = session.capabilities.supportsClipboardContext ? 'clipboard' : elementContext; + const toEvaluate = elements.map(element => element instanceof Variable ? (element.evaluateName || element.value) : element.name); + + try { + const evaluations = await Promise.all(toEvaluate.map(expr => session.evaluate(expr, stackFrame.frameId, evalContext))); + const result = coalesce(evaluations).map(evaluation => evaluation.body.result); + if (result.length) { + clipboardService.writeText(result.join('\n')); + } + } catch (e) { + const result = elements.map(element => element.value); + clipboardService.writeText(result.join('\n')); } } }); @@ -433,3 +458,23 @@ CommandsRegistry.registerCommand({ } }); +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'variables.collapse', + viewId: VARIABLES_VIEW_ID, + title: localize('collapse', "Collapse All"), + f1: false, + icon: Codicon.collapseAll, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', VARIABLES_VIEW_ID) + } + }); + } + + runInView(_accessor: ServicesAccessor, view: VariablesView) { + view.collapseAll(); + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index 2e18414c..23ee7bab 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -3,20 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { CollapseAction } from 'vs/workbench/browser/viewlet'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { IDebugService, IExpression, CONTEXT_WATCH_EXPRESSIONS_FOCUSED } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IExpression, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, WATCH_VIEW_ID, CONTEXT_WATCH_EXPRESSIONS_EXIST, CONTEXT_WATCH_ITEM_TYPE } from 'vs/workbench/contrib/debug/common/debug'; import { Expression, Variable } from 'vs/workbench/contrib/debug/common/debugModel'; -import { AddWatchExpressionAction, RemoveAllWatchExpressionsAction, CopyValueAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IAction, Action, Separator } from 'vs/base/common/actions'; +import { IAction } from 'vs/base/common/actions'; import { renderExpressionValue, renderViewTree, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; @@ -26,13 +23,17 @@ import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { FuzzyScore } from 'vs/base/common/filters'; import { IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { VariablesRenderer } from 'vs/workbench/contrib/debug/browser/variablesView'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, ContextKeyEqualsExpr, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { dispose } from 'vs/base/common/lifecycle'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { debugCollapseAll } from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { watchExpressionsRemoveAll, watchExpressionsAdd } from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { registerAction2, MenuId, Action2, IMenuService, IMenu } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; +import { Codicon } from 'vs/base/common/codicons'; +import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; let ignoreViewUpdates = false; @@ -43,6 +44,9 @@ export class WatchExpressionsView extends ViewPane { private watchExpressionsUpdatedScheduler: RunOnceScheduler; private needsRefresh = false; private tree!: WorkbenchAsyncDataTree; + private watchExpressionsExist: IContextKey; + private watchItemType: IContextKey; + private menu: IMenu; constructor( options: IViewletViewOptions, @@ -56,13 +60,19 @@ export class WatchExpressionsView extends ViewPane { @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, + @IMenuService menuService: IMenuService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + this.menu = menuService.createMenu(MenuId.DebugWatchContext, contextKeyService); + this._register(this.menu); this.watchExpressionsUpdatedScheduler = new RunOnceScheduler(() => { this.needsRefresh = false; this.tree.updateChildren(); }, 50); + this.watchExpressionsExist = CONTEXT_WATCH_EXPRESSIONS_EXIST.bindTo(contextKeyService); + this.watchExpressionsExist.set(this.debugService.getModel().getWatchExpressions().length > 0); + this.watchItemType = CONTEXT_WATCH_ITEM_TYPE.bindTo(contextKeyService); } renderBody(container: HTMLElement): void { @@ -98,6 +108,7 @@ export class WatchExpressionsView extends ViewPane { this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); this._register(this.tree.onMouseDblClick(e => this.onMouseDblClick(e))); this._register(this.debugService.getModel().onDidChangeWatchExpressions(async we => { + this.watchExpressionsExist.set(this.debugService.getModel().getWatchExpressions().length > 0); if (!this.isBodyVisible()) { this.needsRefresh = true; } else { @@ -141,7 +152,10 @@ export class WatchExpressionsView extends ViewPane { this.tree.updateOptions({ horizontalScrolling: false }); } - this.tree.rerender(e); + if (e.name) { + // Only rerender if the input is already done since otherwise the tree is not yet aware of the new element + this.tree.rerender(e); + } } else if (!e && horizontalScrolling !== undefined) { this.tree.updateOptions({ horizontalScrolling: horizontalScrolling }); horizontalScrolling = undefined; @@ -158,12 +172,8 @@ export class WatchExpressionsView extends ViewPane { this.tree.domFocus(); } - getActions(): IAction[] { - return [ - new AddWatchExpressionAction(AddWatchExpressionAction.ID, AddWatchExpressionAction.LABEL, this.debugService, this.keybindingService), - new CollapseAction(() => this.tree, true, 'explorer-action ' + ThemeIcon.asClassName(debugCollapseAll)), - new RemoveAllWatchExpressionsAction(RemoveAllWatchExpressionsAction.ID, RemoveAllWatchExpressionsAction.LABEL, this.debugService, this.keybindingService) - ]; + collapseAll(): void { + this.tree.collapseAll(); } private onMouseDblClick(e: ITreeMouseEvent): void { @@ -184,42 +194,17 @@ export class WatchExpressionsView extends ViewPane { private onContextMenu(e: ITreeContextMenuEvent): void { const element = e.element; - const anchor = e.anchor; - if (!anchor) { - return; - } + const selection = this.tree.getSelection(); + + this.watchItemType.set(element instanceof Expression ? 'expression' : element instanceof Variable ? 'variable' : undefined); const actions: IAction[] = []; - - if (element instanceof Expression) { - const expression = element; - actions.push(new AddWatchExpressionAction(AddWatchExpressionAction.ID, AddWatchExpressionAction.LABEL, this.debugService, this.keybindingService)); - actions.push(new Action('debug.editWatchExpression', nls.localize('editWatchExpression', "Edit Expression"), undefined, true, () => { - this.debugService.getViewModel().setSelectedExpression(expression); - return Promise.resolve(); - })); - actions.push(this.instantiationService.createInstance(CopyValueAction, CopyValueAction.ID, CopyValueAction.LABEL, expression, 'watch')); - actions.push(new Separator()); - - actions.push(new Action('debug.removeWatchExpression', nls.localize('removeWatchExpression', "Remove Expression"), undefined, true, () => { - this.debugService.removeWatchExpressions(expression.getId()); - return Promise.resolve(); - })); - actions.push(new RemoveAllWatchExpressionsAction(RemoveAllWatchExpressionsAction.ID, RemoveAllWatchExpressionsAction.LABEL, this.debugService, this.keybindingService)); - } else { - actions.push(new AddWatchExpressionAction(AddWatchExpressionAction.ID, AddWatchExpressionAction.LABEL, this.debugService, this.keybindingService)); - if (element instanceof Variable) { - const variable = element as Variable; - actions.push(this.instantiationService.createInstance(CopyValueAction, CopyValueAction.ID, CopyValueAction.LABEL, variable, 'watch')); - actions.push(new Separator()); - } - actions.push(new RemoveAllWatchExpressionsAction(RemoveAllWatchExpressionsAction.ID, RemoveAllWatchExpressionsAction.LABEL, this.debugService, this.keybindingService)); - } + const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: element, shouldForwardArgs: true }, actions); this.contextMenuService.showContextMenu({ - getAnchor: () => anchor, + getAnchor: () => e.anchor, getActions: () => actions, - getActionsContext: () => element, - onHide: () => dispose(actions) + getActionsContext: () => element && selection.includes(element) ? selection : element ? [element] : [], + onHide: () => dispose(actionsDisposable) }); } } @@ -287,8 +272,8 @@ export class WatchExpressionsRenderer extends AbstractExpressionsRenderer { protected getInputBoxOptions(expression: IExpression): IInputBoxOptions { return { initialValue: expression.name ? expression.name : '', - ariaLabel: nls.localize('watchExpressionInputAriaLabel', "Type watch expression"), - placeholder: nls.localize('watchExpressionPlaceholder', "Expression to watch"), + ariaLabel: localize('watchExpressionInputAriaLabel', "Type watch expression"), + placeholder: localize('watchExpressionPlaceholder', "Expression to watch"), onFinish: (value: string, success: boolean) => { if (success && value) { this.debugService.renameWatchExpression(expression.getId(), value); @@ -306,16 +291,16 @@ export class WatchExpressionsRenderer extends AbstractExpressionsRenderer { class WatchExpressionsAccessibilityProvider implements IListAccessibilityProvider { getWidgetAriaLabel(): string { - return nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'watchAriaTreeLabel' }, "Debug Watch Expressions"); + return localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'watchAriaTreeLabel' }, "Debug Watch Expressions"); } getAriaLabel(element: IExpression): string { if (element instanceof Expression) { - return nls.localize('watchExpressionAriaLabel', "{0}, value {1}", (element).name, (element).value); + return localize('watchExpressionAriaLabel', "{0}, value {1}", (element).name, (element).value); } // Variable - return nls.localize('watchVariableAriaLabel', "{0}, value {1}", (element).name, (element).value); + return localize('watchVariableAriaLabel', "{0}, value {1}", (element).name, (element).value); } } @@ -359,3 +344,75 @@ class WatchExpressionsDragAndDrop implements ITreeDragAndDrop { this.debugService.moveWatchExpression(draggedElement.getId(), position); } } + +registerAction2(class Collapse extends ViewAction { + constructor() { + super({ + id: 'watch.collapse', + viewId: WATCH_VIEW_ID, + title: localize('collapse', "Collapse All"), + f1: false, + icon: Codicon.collapseAll, + precondition: CONTEXT_WATCH_EXPRESSIONS_EXIST, + menu: { + id: MenuId.ViewTitle, + order: 30, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', WATCH_VIEW_ID) + } + }); + } + + runInView(_accessor: ServicesAccessor, view: WatchExpressionsView) { + view.collapseAll(); + } +}); + +export const ADD_WATCH_ID = 'workbench.debug.viewlet.action.addWatchExpression'; // Use old and long id for backwards compatibility +export const ADD_WATCH_LABEL = localize('addWatchExpression', "Add Expression"); + +registerAction2(class AddWatchExpressionAction extends Action2 { + constructor() { + super({ + id: ADD_WATCH_ID, + title: ADD_WATCH_LABEL, + f1: false, + icon: watchExpressionsAdd, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', WATCH_VIEW_ID) + } + }); + } + + run(accessor: ServicesAccessor): void { + const debugService = accessor.get(IDebugService); + debugService.addWatchExpression(); + } +}); + +export const REMOVE_WATCH_EXPRESSIONS_COMMAND_ID = 'workbench.debug.viewlet.action.removeAllWatchExpressions'; +export const REMOVE_WATCH_EXPRESSIONS_LABEL = localize('removeAllWatchExpressions', "Remove All Expressions"); +registerAction2(class RemoveAllWatchExpressionsAction extends Action2 { + constructor() { + super({ + id: REMOVE_WATCH_EXPRESSIONS_COMMAND_ID, // Use old and long id for backwards compatibility + title: REMOVE_WATCH_EXPRESSIONS_LABEL, + f1: false, + icon: watchExpressionsRemoveAll, + precondition: CONTEXT_WATCH_EXPRESSIONS_EXIST, + menu: { + id: MenuId.ViewTitle, + order: 20, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', WATCH_VIEW_ID) + } + }); + } + + run(accessor: ServicesAccessor): void { + const debugService = accessor.get(IDebugService); + debugService.removeWatchExpressions(); + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/welcomeView.ts b/src/vs/workbench/contrib/debug/browser/welcomeView.ts index c88b28e8..e33671e7 100644 --- a/src/vs/workbench/contrib/debug/browser/welcomeView.ts +++ b/src/vs/workbench/contrib/debug/browser/welcomeView.ts @@ -10,10 +10,9 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService, RawContextKey, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { localize } from 'vs/nls'; -import { StartAction, ConfigureAction, SelectAndStartAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IDebugService, CONTEXT_DEBUGGERS_AVAILABLE } from 'vs/workbench/contrib/debug/common/debug'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IViewDescriptorService, IViewsRegistry, Extensions, ViewContentGroups } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -25,6 +24,7 @@ import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { SELECT_AND_START_ID, DEBUG_CONFIGURE_COMMAND_ID, DEBUG_START_COMMAND_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; const debugStartLanguageKey = 'debugStartLanguage'; const CONTEXT_DEBUG_START_LANGUAGE = new RawContextKey(debugStartLanguageKey, undefined); @@ -96,7 +96,7 @@ export class WelcomeView extends ViewPane { })); setContextKey(); - const debugKeybinding = this.keybindingService.lookupKeybinding(StartAction.ID); + const debugKeybinding = this.keybindingService.lookupKeybinding(DEBUG_START_COMMAND_ID); debugKeybindingLabel = debugKeybinding ? ` (${debugKeybinding.getLabel()})` : ''; } @@ -116,14 +116,14 @@ viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { let debugKeybindingLabel = ''; viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'runAndDebugAction', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, - "[Run and Debug{0}](command:{1})", debugKeybindingLabel, StartAction.ID), + "[Run and Debug{0}](command:{1})", debugKeybindingLabel, DEBUG_START_COMMAND_ID), when: CONTEXT_DEBUGGERS_AVAILABLE, group: ViewContentGroups.Debug }); viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'detectThenRunAndDebug', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, - "[Show](command:{0}) all automatic debug configurations.", SelectAndStartAction.ID), + "[Show](command:{0}) all automatic debug configurations.", SELECT_AND_START_ID), when: CONTEXT_DEBUGGERS_AVAILABLE, group: ViewContentGroups.Debug, order: 10 @@ -131,7 +131,7 @@ viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'customizeRunAndDebug', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, - "To customize Run and Debug [create a launch.json file](command:{0}).", ConfigureAction.ID), + "To customize Run and Debug [create a launch.json file](command:{0}).", DEBUG_CONFIGURE_COMMAND_ID), when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, WorkbenchStateContext.notEqualsTo('empty')), group: ViewContentGroups.Debug }); diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 8f171554..c46778c6 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -25,6 +25,7 @@ import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configur import { CancellationToken } from 'vs/base/common/cancellation'; import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; +import { IAction } from 'vs/base/common/actions'; export const VIEWLET_ID = 'workbench.view.debug'; @@ -47,10 +48,14 @@ export const CONTEXT_BREAKPOINT_WIDGET_VISIBLE = new RawContextKey('bre export const CONTEXT_IN_BREAKPOINT_WIDGET = new RawContextKey('inBreakpointWidget', false); export const CONTEXT_BREAKPOINTS_FOCUSED = new RawContextKey('breakpointsFocused', true); export const CONTEXT_WATCH_EXPRESSIONS_FOCUSED = new RawContextKey('watchExpressionsFocused', true); +export const CONTEXT_WATCH_EXPRESSIONS_EXIST = new RawContextKey('watchExpressionsExist', false); export const CONTEXT_VARIABLES_FOCUSED = new RawContextKey('variablesFocused', true); export const CONTEXT_EXPRESSION_SELECTED = new RawContextKey('expressionSelected', false); -export const CONTEXT_BREAKPOINT_SELECTED = new RawContextKey('breakpointSelected', false); +export const CONTEXT_BREAKPOINT_INPUT_FOCUSED = new RawContextKey('breakpointInputFocused', false); export const CONTEXT_CALLSTACK_ITEM_TYPE = new RawContextKey('callStackItemType', undefined); +export const CONTEXT_WATCH_ITEM_TYPE = new RawContextKey('watchItemType', undefined); +export const CONTEXT_BREAKPOINT_ITEM_TYPE = new RawContextKey('breakpointItemType', undefined); +export const CONTEXT_BREAKPOINT_SUPPORTS_CONDITION = new RawContextKey('breakpointSupportsCondition', false); export const CONTEXT_LOADED_SCRIPTS_SUPPORTED = new RawContextKey('loadedScriptsSupported', false); export const CONTEXT_LOADED_SCRIPTS_ITEM_TYPE = new RawContextKey('loadedScriptsItemType', undefined); export const CONTEXT_FOCUSED_SESSION_IS_ATTACH = new RawContextKey('focusedSessionIsAttach', false); @@ -65,6 +70,8 @@ export const CONTEXT_SET_VARIABLE_SUPPORTED = new RawContextKey('debugS export const CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED = new RawContextKey('breakWhenValueChangesSupported', false); export const CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT = new RawContextKey('variableEvaluateNamePresent', false); export const CONTEXT_EXCEPTION_WIDGET_VISIBLE = new RawContextKey('exceptionWidgetVisible', false); +export const CONTEXT_MULTI_SESSION_REPL = new RawContextKey('multiSessionRepl', false); +export const CONTEXT_MULTI_SESSION_DEBUG = new RawContextKey('multiSessionDebug', false); export const EDITOR_CONTRIBUTION_ID = 'editor.contrib.debug'; export const BREAKPOINT_EDITOR_CONTRIBUTION_ID = 'editor.contrib.breakpoint'; @@ -183,6 +190,7 @@ export interface IDebugSession extends ITreeElement { readonly subId: string | undefined; readonly compact: boolean; readonly compoundRoot: DebugCompoundRoot | undefined; + readonly name: string; setSubId(subId: string | undefined): void; @@ -437,9 +445,7 @@ export interface IViewModel extends ITreeElement { readonly focusedStackFrame: IStackFrame | undefined; getSelectedExpression(): IExpression | undefined; - getSelectedBreakpoint(): IFunctionBreakpoint | IExceptionBreakpoint | undefined; setSelectedExpression(expression: IExpression | undefined): void; - setSelectedBreakpoint(functionBreakpoint: IFunctionBreakpoint | IExceptionBreakpoint | undefined): void; updateViews(): void; isMultiSessionView(): boolean; @@ -629,7 +635,6 @@ export interface IDebuggerContribution extends IPlatformSpecificAdapterContribut // supported languages languages?: string[]; - enableBreakpointsFor?: { languageIds?: string[] }; // debug configuration support configurationAttributes?: any; @@ -845,10 +850,10 @@ export interface IDebugService { addFunctionBreakpoint(name?: string, id?: string): void; /** - * Renames an already existing function breakpoint. + * Updates an already existing function breakpoint. * Notifies debug adapter of breakpoint changes. */ - renameFunctionBreakpoint(id: string, newFunctionName: string): Promise; + updateFunctionBreakpoint(id: string, update: { name?: string, hitCondition?: string, condition?: string }): Promise; /** * Removes all function breakpoints. If id is passed only removes the function breakpoint with the passed id. @@ -869,6 +874,8 @@ export interface IDebugService { setExceptionBreakpointCondition(breakpoint: IExceptionBreakpoint, condition: string | undefined): Promise; + setExceptionBreakpoints(data: DebugProtocol.ExceptionBreakpointsFilter[]): void; + /** * Sends all breakpoints to the passed session. * If session is not passed, sends all breakpoints to each session. @@ -947,6 +954,7 @@ export interface IDebugEditorContribution extends editorCommon.IEditorContributi export interface IBreakpointEditorContribution extends editorCommon.IEditorContribution { showBreakpointWidget(lineNumber: number, column: number | undefined, context?: BreakpointWidgetContext): void; closeBreakpointWidget(): void; + getContextMenuActionsAtPosition(lineNumber: number, model: EditorIModel): IAction[]; } // temporary debug helper service diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index b98b3b89..68184d87 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -423,7 +423,9 @@ export class Thread implements IThread { } getTopStackFrame(): IStackFrame | undefined { - return this.getCallStack().find(sf => !!(sf && sf.source && sf.source.available && sf.source.presentationHint !== 'deemphasize')); + const callStack = this.getCallStack(); + const firstAvailableStackFrame = callStack.find(sf => !!(sf && sf.source && sf.source.available && sf.source.presentationHint !== 'deemphasize')); + return firstAvailableStackFrame || (callStack.length > 0 ? callStack[0] : undefined); } get stateLabel(): string { @@ -452,6 +454,9 @@ export class Thread implements IThread { this.callStack.splice(start, this.callStack.length - start); } this.callStack = this.callStack.concat(callStack || []); + if (typeof this.stoppedDetails?.totalFrames === 'number' && this.stoppedDetails.totalFrames === this.callStack.length) { + this.reachedEndOfCallStack = true; + } } } @@ -946,6 +951,11 @@ export class DebugModel implements IDebugModel { return true; }); + let i = 1; + while (this.sessions.some(s => s.getLabel() === session.getLabel())) { + session.setName(`${session.configuration.name} ${++i}`); + } + let index = -1; if (session.parentSession) { // Make sure that child sessions are placed after the parent session @@ -1236,10 +1246,18 @@ export class DebugModel implements IDebugModel { return newFunctionBreakpoint; } - renameFunctionBreakpoint(id: string, name: string): void { + updateFunctionBreakpoint(id: string, update: { name?: string, hitCondition?: string, condition?: string }): void { const functionBreakpoint = this.functionBreakpoints.find(fbp => fbp.getId() === id); if (functionBreakpoint) { - functionBreakpoint.name = name; + if (typeof update.name === 'string') { + functionBreakpoint.name = update.name; + } + if (typeof update.condition === 'string') { + functionBreakpoint.condition = update.condition; + } + if (typeof update.hitCondition === 'string') { + functionBreakpoint.hitCondition = update.hitCondition; + } this._onDidChangeBreakpoints.fire({ changed: [functionBreakpoint], sessionOnly: false }); } } diff --git a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index f11bab4f..dad39a58 100644 --- a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -740,7 +740,7 @@ declare module DebugProtocol { /** Reference to the Variable container if the data breakpoint is requested for a child of the container. */ variablesReference?: number; /** The name of the Variable's child to obtain data breakpoint information for. - If variableReference isn’t provided, this can be an expression. + If variablesReference isn’t provided, this can be an expression. */ name: string; } @@ -1012,7 +1012,7 @@ declare module DebugProtocol { /** StackTrace request; value of command field is 'stackTrace'. The request returns a stacktrace from the current execution state of a given thread. - A client can request all stack frames by omitting the startFrame and levels arguments. For performance conscious clients stack frames can be retrieved in a piecemeal way with the startFrame and levels arguments. The response of the stackTrace request may contain a totalFrames property that hints at the total number of frames in the stack. If a client needs this total number upfront, it can issue a request for a single (first) frame and depending on the value of totalFrames decide how to proceed. In any case a client should be prepared to receive less frames than requested, which is an indication that the end of the stack has been reached. + A client can request all stack frames by omitting the startFrame and levels arguments. For performance conscious clients and if the debug adapter's 'supportsDelayedStackTraceLoading' capability is true, stack frames can be retrieved in a piecemeal way with the startFrame and levels arguments. The response of the stackTrace request may contain a totalFrames property that hints at the total number of frames in the stack. If a client needs this total number upfront, it can issue a request for a single (first) frame and depending on the value of totalFrames decide how to proceed. In any case a client should be prepared to receive less frames than requested, which is an indication that the end of the stack has been reached. */ export interface StackTraceRequest extends Request { // command: 'stackTrace'; @@ -1591,7 +1591,7 @@ declare module DebugProtocol { supportsExceptionInfoRequest?: boolean; /** The debug adapter supports the 'terminateDebuggee' attribute on the 'disconnect' request. */ supportTerminateDebuggee?: boolean; - /** The debug adapter supports the delayed loading of parts of the stack, which requires that both the 'startFrame' and 'levels' arguments and the 'totalFrames' result of the 'StackTrace' request are supported. */ + /** The debug adapter supports the delayed loading of parts of the stack, which requires that both the 'startFrame' and 'levels' arguments and an optional 'totalFrames' result of the 'StackTrace' request are supported. */ supportsDelayedStackTraceLoading?: boolean; /** The debug adapter supports the 'loadedSources' request. */ supportsLoadedSourcesRequest?: boolean; @@ -1871,7 +1871,7 @@ declare module DebugProtocol { 'mostDerivedClass': Indicates that the object is the most derived class. 'virtual': Indicates that the object is virtual, that means it is a synthetic object introducedby the adapter for rendering purposes, e.g. an index range for large arrays. - 'dataBreakpoint': Indicates that a data breakpoint is registered for the object. + 'dataBreakpoint': Deprecated: Indicates that a data breakpoint is registered for the object. The 'hasDataBreakpoint' attribute should generally be used instead. etc. */ kind?: 'property' | 'method' | 'class' | 'data' | 'event' | 'baseClass' | 'innerClass' | 'interface' | 'mostDerivedClass' | 'virtual' | 'dataBreakpoint' | string; @@ -1884,9 +1884,10 @@ declare module DebugProtocol { 'hasObjectId': Indicates that the object can have an Object ID created for it. 'canHaveObjectId': Indicates that the object has an Object ID associated with it. 'hasSideEffects': Indicates that the evaluation had side effects. + 'hasDataBreakpoint': Indicates that the object has its value tracked by a data breakpoint. etc. */ - attributes?: ('static' | 'constant' | 'readOnly' | 'rawString' | 'hasObjectId' | 'canHaveObjectId' | 'hasSideEffects' | string)[]; + attributes?: ('static' | 'constant' | 'readOnly' | 'rawString' | 'hasObjectId' | 'canHaveObjectId' | 'hasSideEffects' | 'hasDataBreakpoint' | string)[]; /** Visibility of variable. Before introducing additional values, try to use the listed values. Values: 'public', 'private', 'protected', 'internal', 'final', etc. */ diff --git a/src/vs/workbench/contrib/debug/common/debugViewModel.ts b/src/vs/workbench/contrib/debug/common/debugViewModel.ts index e7541cd8..a17e6118 100644 --- a/src/vs/workbench/contrib/debug/common/debugViewModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugViewModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; -import { CONTEXT_EXPRESSION_SELECTED, IViewModel, IStackFrame, IDebugSession, IThread, IExpression, IFunctionBreakpoint, CONTEXT_BREAKPOINT_SELECTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, IExceptionBreakpoint } from 'vs/workbench/contrib/debug/common/debug'; +import { CONTEXT_EXPRESSION_SELECTED, IViewModel, IStackFrame, IDebugSession, IThread, IExpression, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG } from 'vs/workbench/contrib/debug/common/debug'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; @@ -16,14 +16,11 @@ export class ViewModel implements IViewModel { private _focusedSession: IDebugSession | undefined; private _focusedThread: IThread | undefined; private selectedExpression: IExpression | undefined; - private selectedBreakpoint: IFunctionBreakpoint | IExceptionBreakpoint | undefined; private readonly _onDidFocusSession = new Emitter(); private readonly _onDidFocusStackFrame = new Emitter<{ stackFrame: IStackFrame | undefined, explicit: boolean }>(); private readonly _onDidSelectExpression = new Emitter(); private readonly _onWillUpdateViews = new Emitter(); - private multiSessionView: boolean; private expressionSelectedContextKey!: IContextKey; - private breakpointSelectedContextKey!: IContextKey; private loadedScriptsSupportedContextKey!: IContextKey; private stepBackSupportedContextKey!: IContextKey; private focusedSessionIsAttach!: IContextKey; @@ -31,12 +28,11 @@ export class ViewModel implements IViewModel { private stepIntoTargetsSupported!: IContextKey; private jumpToCursorSupported!: IContextKey; private setVariableSupported!: IContextKey; + private multiSessionDebug!: IContextKey; constructor(private contextKeyService: IContextKeyService) { - this.multiSessionView = false; contextKeyService.bufferChangeEvents(() => { this.expressionSelectedContextKey = CONTEXT_EXPRESSION_SELECTED.bindTo(contextKeyService); - this.breakpointSelectedContextKey = CONTEXT_BREAKPOINT_SELECTED.bindTo(contextKeyService); this.loadedScriptsSupportedContextKey = CONTEXT_LOADED_SCRIPTS_SUPPORTED.bindTo(contextKeyService); this.stepBackSupportedContextKey = CONTEXT_STEP_BACK_SUPPORTED.bindTo(contextKeyService); this.focusedSessionIsAttach = CONTEXT_FOCUSED_SESSION_IS_ATTACH.bindTo(contextKeyService); @@ -44,6 +40,7 @@ export class ViewModel implements IViewModel { this.stepIntoTargetsSupported = CONTEXT_STEP_INTO_TARGETS_SUPPORTED.bindTo(contextKeyService); this.jumpToCursorSupported = CONTEXT_JUMP_TO_CURSOR_SUPPORTED.bindTo(contextKeyService); this.setVariableSupported = CONTEXT_SET_VARIABLE_SUPPORTED.bindTo(contextKeyService); + this.multiSessionDebug = CONTEXT_MULTI_SESSION_DEBUG.bindTo(contextKeyService); }); } @@ -112,10 +109,6 @@ export class ViewModel implements IViewModel { return this._onDidSelectExpression.event; } - getSelectedBreakpoint(): IFunctionBreakpoint | IExceptionBreakpoint | undefined { - return this.selectedBreakpoint; - } - updateViews(): void { this._onWillUpdateViews.fire(); } @@ -124,16 +117,11 @@ export class ViewModel implements IViewModel { return this._onWillUpdateViews.event; } - setSelectedBreakpoint(breakpoint: IFunctionBreakpoint | IExceptionBreakpoint | undefined): void { - this.selectedBreakpoint = breakpoint; - this.breakpointSelectedContextKey.set(!!breakpoint); - } - isMultiSessionView(): boolean { - return this.multiSessionView; + return !!this.multiSessionDebug.get(); } setMultiSessionView(isMultiSessionView: boolean): void { - this.multiSessionView = isMultiSessionView; + this.multiSessionDebug.set(isMultiSessionView); } } diff --git a/src/vs/workbench/contrib/debug/common/replModel.ts b/src/vs/workbench/contrib/debug/common/replModel.ts index f10608a0..c46e6b9c 100644 --- a/src/vs/workbench/contrib/debug/common/replModel.ts +++ b/src/vs/workbench/contrib/debug/common/replModel.ts @@ -15,6 +15,7 @@ import { Emitter, Event } from 'vs/base/common/event'; const MAX_REPL_LENGTH = 10000; let topReplElementCounter = 0; +const getUniqueId = () => `topReplElement:${topReplElementCounter++}`; export class SimpleReplElement implements IReplElement { @@ -30,8 +31,12 @@ export class SimpleReplElement implements IReplElement { ) { } toString(): string { + let valueRespectCount = this.value; + for (let i = 1; i < this.count; i++) { + valueRespectCount += (valueRespectCount.endsWith('\n') ? '' : '\n') + this.value; + } const sourceStr = this.sourceData ? ` ${this.sourceData.source.name}` : ''; - return this.value + sourceStr; + return valueRespectCount + sourceStr; } getId(): string { @@ -226,13 +231,14 @@ export class ReplModel { return; } if (!previousElement.value.endsWith('\n') && !previousElement.value.endsWith('\r\n') && previousElement.count === 1) { - previousElement.value += data; + this.replElements[this.replElements.length - 1] = new SimpleReplElement( + session, getUniqueId(), previousElement.value + data, sev, source); this._onDidChangeElements.fire(); return; } } - const element = new SimpleReplElement(session, `topReplElement:${topReplElementCounter++}`, data, sev, source); + const element = new SimpleReplElement(session, getUniqueId(), data, sev, source); this.addReplElement(element); } else { // TODO@Isidor hack, we should introduce a new type which is an output that can fetch children like an expression @@ -307,7 +313,7 @@ export class ReplModel { } // show object - this.appendToRepl(session, new RawObjectReplElement(`topReplElement:${topReplElementCounter++}`, (a).prototype, a, undefined, nls.localize('snapshotObj', "Only primitive values are shown for this object.")), sev, source); + this.appendToRepl(session, new RawObjectReplElement(getUniqueId(), (a).prototype, a, undefined, nls.localize('snapshotObj', "Only primitive values are shown for this object.")), sev, source); } // string: watch out for % replacement directive diff --git a/src/vs/workbench/contrib/debug/node/debugHelperService.ts b/src/vs/workbench/contrib/debug/node/debugHelperService.ts index a7929124..05be0f7d 100644 --- a/src/vs/workbench/contrib/debug/node/debugHelperService.ts +++ b/src/vs/workbench/contrib/debug/node/debugHelperService.ts @@ -5,7 +5,7 @@ import { IDebugHelperService } from 'vs/workbench/contrib/debug/common/debug'; import { Client as TelemetryClient } from 'vs/base/parts/ipc/node/ipc.cp'; -import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc'; +import { TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc'; import { FileAccess } from 'vs/base/common/network'; import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -31,8 +31,8 @@ export class NodeDebugHelperService implements IDebugHelperService { args: args, env: { ELECTRON_RUN_AS_NODE: 1, - PIPE_LOGGING: 'true', - AMD_ENTRYPOINT: 'vs/workbench/contrib/debug/node/telemetryApp' + VSCODE_PIPE_LOGGING: 'true', + VSCODE_AMD_ENTRYPOINT: 'vs/workbench/contrib/debug/node/telemetryApp' } } ); diff --git a/src/vs/workbench/contrib/debug/node/telemetryApp.ts b/src/vs/workbench/contrib/debug/node/telemetryApp.ts index 52507e4f..85597832 100644 --- a/src/vs/workbench/contrib/debug/node/telemetryApp.ts +++ b/src/vs/workbench/contrib/debug/node/telemetryApp.ts @@ -5,7 +5,7 @@ import { Server } from 'vs/base/parts/ipc/node/ipc.cp'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; -import { TelemetryAppenderChannel } from 'vs/platform/telemetry/node/telemetryIpc'; +import { TelemetryAppenderChannel } from 'vs/platform/telemetry/common/telemetryIpc'; const appender = new AppInsightsAppender(process.argv[2], JSON.parse(process.argv[3]), process.argv[4]); process.once('exit', () => appender.flush()); diff --git a/src/vs/workbench/contrib/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index 45a8fc65..03c4fc8f 100644 --- a/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/src/vs/workbench/contrib/debug/node/terminals.ts @@ -9,7 +9,7 @@ import { WindowsExternalTerminalService, MacExternalTerminalService, LinuxExtern import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IExternalTerminalService } from 'vs/workbench/contrib/externalTerminal/common/externalTerminal'; import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; -import { extractDriveLetter } from 'vs/base/common/labels'; +import { getDriveLetter } from 'vs/base/common/extpath'; let externalTerminalService: IExternalTerminalService | undefined = undefined; @@ -112,7 +112,7 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env? }; if (cwd) { - const driveLetter = extractDriveLetter(cwd); + const driveLetter = getDriveLetter(cwd); if (driveLetter) { command += `${driveLetter}:; `; } @@ -145,7 +145,7 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env? }; if (cwd) { - const driveLetter = extractDriveLetter(cwd); + const driveLetter = getDriveLetter(cwd); if (driveLetter) { command += `${driveLetter}: && `; } diff --git a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts index 1a2bed97..fec15f39 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -153,8 +153,8 @@ suite('Debug - Breakpoints', () => { test('function breakpoints', () => { model.addFunctionBreakpoint('foo', '1'); model.addFunctionBreakpoint('bar', '2'); - model.renameFunctionBreakpoint('1', 'fooUpdated'); - model.renameFunctionBreakpoint('2', 'barUpdated'); + model.updateFunctionBreakpoint('1', { name: 'fooUpdated' }); + model.updateFunctionBreakpoint('2', { name: 'barUpdated' }); const functionBps = model.getFunctionBreakpoints(); assert.equal(functionBps[0].name, 'fooUpdated'); diff --git a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts index ca7c0db5..dd694374 100644 --- a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts @@ -20,6 +20,14 @@ import { generateUuid } from 'vs/base/common/uuid'; import { debugStackframe, debugStackframeFocused } from 'vs/workbench/contrib/debug/browser/debugIcons'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +const mockWorkspaceContextService = { + getWorkspace: () => { + return { + folders: [] + }; + } +} as any; + export function createMockSession(model: DebugModel, name = 'mockSession', options?: IDebugSessionOptions): DebugSession { return new DebugSession(generateUuid(), { resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, options, { getViewModel(): any { @@ -29,7 +37,7 @@ export function createMockSession(model: DebugModel, name = 'mockSession', optio } }; } - } as IDebugService, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!, undefined!, mockUriIdentityService); + } as IDebugService, undefined!, undefined!, undefined!, undefined!, mockWorkspaceContextService, undefined!, undefined!, NullOpenerService, undefined!, undefined!, mockUriIdentityService); } function createTwoStackFrames(session: DebugSession): { firstStackFrame: StackFrame, secondStackFrame: StackFrame } { @@ -91,7 +99,7 @@ suite('Debug - CallStack', () => { assert.equal(model.getSessions(true).length, 1); }); - test('threads multiple wtih allThreadsStopped', () => { + test('threads multiple wtih allThreadsStopped', async () => { const threadId1 = 1; const threadName1 = 'firstThread'; const threadId2 = 2; @@ -145,19 +153,16 @@ suite('Debug - CallStack', () => { // 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); - }); + await thread1.fetchCallStack(); + assert.notEqual(thread1.getCallStack().length, 0); - thread2.fetchCallStack().then(() => { - assert.notEqual(thread2.getCallStack().length, 0); - }); + await thread2.fetchCallStack(); + 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(); - }); + await thread1.fetchCallStack(); + await thread2.fetchCallStack(); // clearing the callstack results in the callstack not being available thread1.clearCallStack(); @@ -174,7 +179,7 @@ suite('Debug - CallStack', () => { assert.equal(session.getAllThreads().length, 0); }); - test('threads mutltiple without allThreadsStopped', () => { + test('threads mutltiple without allThreadsStopped', async () => { const sessionStub = sinon.spy(rawSession, 'stackTrace'); const stoppedThreadId = 1; @@ -230,19 +235,17 @@ suite('Debug - CallStack', () => { // 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); - }); + await stoppedThread.fetchCallStack(); + 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); - }); + await runningThread.fetchCallStack(); + assert.equal(runningThread.getCallStack().length, 0); + assert.equal(sessionStub.callCount, 1); // clearing the callstack results in the callstack not being available stoppedThread.clearCallStack(); @@ -374,7 +377,7 @@ suite('Debug - CallStack', () => { get state(): State { return State.Stopped; } - }(generateUuid(), { resolved: { name: 'stoppedSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!, undefined!, mockUriIdentityService); + }(generateUuid(), { resolved: { name: 'stoppedSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, mockWorkspaceContextService, undefined!, undefined!, NullOpenerService, undefined!, undefined!, mockUriIdentityService); const runningSession = createMockSession(model); model.addSession(runningSession); diff --git a/src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts similarity index 100% rename from src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts rename to src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts diff --git a/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts b/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts index b22d2044..55264234 100644 --- a/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts @@ -64,7 +64,7 @@ suite('Debug - Link Detector', () => { test('singleLineLink', () => { const input = isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34'; - const expectedOutput = isWindows ? 'C:\\foo\\bar.js:12:34<\/a><\/span>' : '/Users/foo/bar.js:12:34<\/a><\/span>'; + const expectedOutput = isWindows ? 'C:\\foo\\bar.js:12:34<\/a><\/span>' : '/Users/foo/bar.js:12:34<\/a><\/span>'; const output = linkDetector.linkify(input); assert.equal(1, output.children.length); @@ -87,7 +87,7 @@ suite('Debug - Link Detector', () => { test('relativeLinkWithWorkspace', () => { const input = '\./foo/bar.js'; - const expectedOutput = /^\.\/foo\/bar\.js<\/a><\/span>$/; + const expectedOutput = /^\.\/foo\/bar\.js<\/a><\/span>$/; const output = linkDetector.linkify(input, false, new WorkspaceFolder({ uri: URI.file('/path/to/workspace'), name: 'ws', index: 0 })); assert.equal('SPAN', output.tagName); @@ -96,7 +96,7 @@ suite('Debug - Link Detector', () => { test('singleLineLinkAndText', function () { const input = isWindows ? 'The link: C:/foo/bar.js:12:34' : 'The link: /Users/foo/bar.js:12:34'; - const expectedOutput = /^The link: .*\/foo\/bar.js:12:34<\/a><\/span>$/; + const expectedOutput = /^The link: .*\/foo\/bar.js:12:34<\/a><\/span>$/; const output = linkDetector.linkify(input); assert.equal(1, output.children.length); @@ -110,7 +110,7 @@ suite('Debug - Link Detector', () => { test('singleLineMultipleLinks', () => { const input = isWindows ? 'Here is a link C:/foo/bar.js:12:34 and here is another D:/boo/far.js:56:78' : 'Here is a link /Users/foo/bar.js:12:34 and here is another /Users/boo/far.js:56:78'; - const expectedOutput = /^Here is a link .*\/foo\/bar.js:12:34<\/a> and here is another .*\/boo\/far.js:56:78<\/a><\/span>$/; + const expectedOutput = /^Here is a link .*\/foo\/bar.js:12:34<\/a> and here is another .*\/boo\/far.js:56:78<\/a><\/span>$/; const output = linkDetector.linkify(input); assert.equal(2, output.children.length); @@ -152,7 +152,7 @@ suite('Debug - Link Detector', () => { test('multilineWithLinks', () => { const input = isWindows ? 'I have a link for you\nHere it is: C:/foo/bar.js:12:34\nCool, huh?' : 'I have a link for you\nHere it is: /Users/foo/bar.js:12:34\nCool, huh?'; - const expectedOutput = /^I have a link for you\n<\/span>Here it is: .*\/foo\/bar.js:12:34<\/a>\n<\/span>Cool, huh\?<\/span><\/span>$/; + const expectedOutput = /^I have a link for you\n<\/span>Here it is: .*\/foo\/bar.js:12:34<\/a>\n<\/span>Cool, huh\?<\/span><\/span>$/; const output = linkDetector.linkify(input, true); assert.equal(3, output.children.length); diff --git a/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts b/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts index 96a346ab..8983f96e 100644 --- a/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts @@ -24,29 +24,29 @@ export const mockUriIdentityService = new UriIdentityService(fileService); export class MockDebugService implements IDebugService { - public _serviceBrand: undefined; + _serviceBrand: undefined; - public get state(): State { + get state(): State { throw new Error('not implemented'); } - public get onWillNewSession(): Event { + get onWillNewSession(): Event { throw new Error('not implemented'); } - public get onDidNewSession(): Event { + get onDidNewSession(): Event { throw new Error('not implemented'); } - public get onDidEndSession(): Event { + get onDidEndSession(): Event { throw new Error('not implemented'); } - public get onDidChangeState(): Event { + get onDidChangeState(): Event { throw new Error('not implemented'); } - public getConfigurationManager(): IConfigurationManager { + getConfigurationManager(): IConfigurationManager { throw new Error('not implemented'); } @@ -58,7 +58,7 @@ export class MockDebugService implements IDebugService { throw new Error('Method not implemented.'); } - public focusStackFrame(focusedStackFrame: IStackFrame): Promise { + focusStackFrame(focusedStackFrame: IStackFrame): Promise { throw new Error('not implemented'); } @@ -66,23 +66,23 @@ export class MockDebugService implements IDebugService { throw new Error('not implemented'); } - public addBreakpoints(uri: uri, rawBreakpoints: IBreakpointData[]): Promise { + addBreakpoints(uri: uri, rawBreakpoints: IBreakpointData[]): Promise { throw new Error('not implemented'); } - public updateBreakpoints(uri: uri, data: Map, sendOnResourceSaved: boolean): Promise { + updateBreakpoints(uri: uri, data: Map, sendOnResourceSaved: boolean): Promise { throw new Error('not implemented'); } - public enableOrDisableBreakpoints(enabled: boolean): Promise { + enableOrDisableBreakpoints(enabled: boolean): Promise { throw new Error('not implemented'); } - public setBreakpointsActivated(): Promise { + setBreakpointsActivated(): Promise { throw new Error('not implemented'); } - public removeBreakpoints(): Promise { + removeBreakpoints(): Promise { throw new Error('not implemented'); } @@ -90,15 +90,19 @@ export class MockDebugService implements IDebugService { throw new Error('Method not implemented.'); } - public addFunctionBreakpoint(): void { } + setExceptionBreakpoints(data: DebugProtocol.ExceptionBreakpointsFilter[]): void { + throw new Error('Method not implemented.'); + } - public moveWatchExpression(id: string, position: number): void { } + addFunctionBreakpoint(): void { } - public renameFunctionBreakpoint(id: string, newFunctionName: string): Promise { + moveWatchExpression(id: string, position: number): void { } + + updateFunctionBreakpoint(id: string, update: { name?: string, hitCondition?: string, condition?: string }): Promise { throw new Error('not implemented'); } - public removeFunctionBreakpoints(id?: string): Promise { + removeFunctionBreakpoints(id?: string): Promise { throw new Error('not implemented'); } @@ -109,47 +113,47 @@ export class MockDebugService implements IDebugService { throw new Error('Method not implemented.'); } - public addReplExpression(name: string): Promise { + addReplExpression(name: string): Promise { throw new Error('not implemented'); } - public removeReplExpressions(): void { } + removeReplExpressions(): void { } - public addWatchExpression(name?: string): Promise { + addWatchExpression(name?: string): Promise { throw new Error('not implemented'); } - public renameWatchExpression(id: string, newName: string): Promise { + renameWatchExpression(id: string, newName: string): Promise { throw new Error('not implemented'); } - public removeWatchExpressions(id?: string): void { } + removeWatchExpressions(id?: string): void { } - public startDebugging(launch: ILaunch, configOrName?: IConfig | string, options?: IDebugSessionOptions): Promise { + startDebugging(launch: ILaunch, configOrName?: IConfig | string, options?: IDebugSessionOptions): Promise { return Promise.resolve(true); } - public restartSession(): Promise { + restartSession(): Promise { throw new Error('not implemented'); } - public stopSession(): Promise { + stopSession(): Promise { throw new Error('not implemented'); } - public getModel(): IDebugModel { + getModel(): IDebugModel { throw new Error('not implemented'); } - public getViewModel(): IViewModel { + getViewModel(): IViewModel { throw new Error('not implemented'); } - public logToRepl(session: IDebugSession, value: string): void { } + logToRepl(session: IDebugSession, value: string): void { } - public sourceIsNotAvailable(uri: uri): void { } + sourceIsNotAvailable(uri: uri): void { } - public tryToAutoFocusStackFrame(thread: IThread): Promise { + tryToAutoFocusStackFrame(thread: IThread): Promise { throw new Error('not implemented'); } } @@ -227,6 +231,10 @@ export class MockSession implements IDebugSession { return 'mockname'; } + get name(): string { + return 'mockname'; + } + setName(name: string): void { throw new Error('not implemented'); } @@ -387,14 +395,14 @@ export class MockRawSession { disconnected = false; sessionLengthInSeconds: number = 0; - public readyForBreakpoints = true; - public emittedStopped = true; + readyForBreakpoints = true; + emittedStopped = true; - public getLengthInSeconds(): number { + getLengthInSeconds(): number { return 100; } - public stackTrace(args: DebugProtocol.StackTraceArguments): Promise { + stackTrace(args: DebugProtocol.StackTraceArguments): Promise { return Promise.resolve({ seq: 1, type: 'response', @@ -412,19 +420,19 @@ export class MockRawSession { }); } - public exceptionInfo(args: DebugProtocol.ExceptionInfoArguments): Promise { + exceptionInfo(args: DebugProtocol.ExceptionInfoArguments): Promise { throw new Error('not implemented'); } - public launchOrAttach(args: IConfig): Promise { + launchOrAttach(args: IConfig): Promise { throw new Error('not implemented'); } - public scopes(args: DebugProtocol.ScopesArguments): Promise { + scopes(args: DebugProtocol.ScopesArguments): Promise { throw new Error('not implemented'); } - public variables(args: DebugProtocol.VariablesArguments): Promise { + variables(args: DebugProtocol.VariablesArguments): Promise { throw new Error('not implemented'); } @@ -432,87 +440,87 @@ export class MockRawSession { return Promise.resolve(null!); } - public custom(request: string, args: any): Promise { + custom(request: string, args: any): Promise { throw new Error('not implemented'); } - public terminate(restart = false): Promise { + terminate(restart = false): Promise { throw new Error('not implemented'); } - public disconnect(restart?: boolean): Promise { + disconnect(restart?: boolean): Promise { throw new Error('not implemented'); } - public threads(): Promise { + threads(): Promise { throw new Error('not implemented'); } - public stepIn(args: DebugProtocol.StepInArguments): Promise { + stepIn(args: DebugProtocol.StepInArguments): Promise { throw new Error('not implemented'); } - public stepOut(args: DebugProtocol.StepOutArguments): Promise { + stepOut(args: DebugProtocol.StepOutArguments): Promise { throw new Error('not implemented'); } - public stepBack(args: DebugProtocol.StepBackArguments): Promise { + stepBack(args: DebugProtocol.StepBackArguments): Promise { throw new Error('not implemented'); } - public continue(args: DebugProtocol.ContinueArguments): Promise { + continue(args: DebugProtocol.ContinueArguments): Promise { throw new Error('not implemented'); } - public reverseContinue(args: DebugProtocol.ReverseContinueArguments): Promise { + reverseContinue(args: DebugProtocol.ReverseContinueArguments): Promise { throw new Error('not implemented'); } - public pause(args: DebugProtocol.PauseArguments): Promise { + pause(args: DebugProtocol.PauseArguments): Promise { throw new Error('not implemented'); } - public terminateThreads(args: DebugProtocol.TerminateThreadsArguments): Promise { + terminateThreads(args: DebugProtocol.TerminateThreadsArguments): Promise { throw new Error('not implemented'); } - public setVariable(args: DebugProtocol.SetVariableArguments): Promise { + setVariable(args: DebugProtocol.SetVariableArguments): Promise { throw new Error('not implemented'); } - public restartFrame(args: DebugProtocol.RestartFrameArguments): Promise { + restartFrame(args: DebugProtocol.RestartFrameArguments): Promise { throw new Error('not implemented'); } - public completions(args: DebugProtocol.CompletionsArguments): Promise { + completions(args: DebugProtocol.CompletionsArguments): Promise { throw new Error('not implemented'); } - public next(args: DebugProtocol.NextArguments): Promise { + next(args: DebugProtocol.NextArguments): Promise { throw new Error('not implemented'); } - public source(args: DebugProtocol.SourceArguments): Promise { + source(args: DebugProtocol.SourceArguments): Promise { throw new Error('not implemented'); } - public loadedSources(args: DebugProtocol.LoadedSourcesArguments): Promise { + loadedSources(args: DebugProtocol.LoadedSourcesArguments): Promise { throw new Error('not implemented'); } - public setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): Promise { + setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): Promise { throw new Error('not implemented'); } - public setFunctionBreakpoints(args: DebugProtocol.SetFunctionBreakpointsArguments): Promise { + setFunctionBreakpoints(args: DebugProtocol.SetFunctionBreakpointsArguments): Promise { throw new Error('not implemented'); } - public setExceptionBreakpoints(args: DebugProtocol.SetExceptionBreakpointsArguments): Promise { + setExceptionBreakpoints(args: DebugProtocol.SetExceptionBreakpointsArguments): Promise { throw new Error('not implemented'); } - public readonly onDidStop: Event = null!; + readonly onDidStop: Event = null!; } export class MockDebugAdapter extends AbstractDebugAdapter { diff --git a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts index 288fa962..e8826837 100644 --- a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts @@ -74,11 +74,11 @@ suite('Debug - REPL', () => { repl.appendToRepl(session, 'third line', severity.Info); elements = repl.getReplElements(); assert.equal(elements.length, 3); - assert.equal(elements[0], 'first line\n'); + assert.equal(elements[0].value, 'first line\n'); assert.equal(elements[0].count, 3); - assert.equal(elements[1], 'second line'); + assert.equal(elements[1].value, 'second line'); assert.equal(elements[1].count, 2); - assert.equal(elements[2], 'third line'); + assert.equal(elements[2].value, 'third line'); assert.equal(elements[2].count, 1); }); @@ -93,11 +93,13 @@ suite('Debug - REPL', () => { repl.appendToRepl(session, 'third line', severity.Info); const elements = repl.getReplElements(); assert.equal(elements.length, 3); - assert.equal(elements[0], 'first line\n'); + assert.equal(elements[0].value, 'first line\n'); + assert.equal(elements[0].toString(), 'first line\nfirst line\nfirst line\n'); assert.equal(elements[0].count, 3); - assert.equal(elements[1], 'second line'); + assert.equal(elements[1].value, 'second line'); + assert.equal(elements[1].toString(), 'second line\nsecond line'); assert.equal(elements[1].count, 2); - assert.equal(elements[2], 'third line'); + assert.equal(elements[2].value, 'third line'); assert.equal(elements[2].count, 1); }); diff --git a/src/vs/workbench/contrib/debug/test/node/debugger.test.ts b/src/vs/workbench/contrib/debug/test/node/debugger.test.ts index 68a34b4b..fd61d857 100644 --- a/src/vs/workbench/contrib/debug/test/node/debugger.test.ts +++ b/src/vs/workbench/contrib/debug/test/node/debugger.test.ts @@ -11,7 +11,7 @@ import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { URI } from 'vs/base/common/uri'; import { ExecutableDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; -import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/modelService.test'; +import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/testTextResourcePropertiesService'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; diff --git a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts index 96c54f0a..feae8000 100644 --- a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts @@ -27,7 +27,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ILabelService } from 'vs/platform/label/common/label'; -import { renderCodicons } from 'vs/base/browser/codicons'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { Schemas } from 'vs/base/common/network'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -297,21 +297,28 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { const activationId = activationTimes.activationReason.extensionId.value; const activationEvent = activationTimes.activationReason.activationEvent; if (activationEvent === '*') { - title = nls.localize('starActivation', "Activated by {0} on start-up", activationId); + title = nls.localize({ + key: 'starActivation', + comment: [ + '{0} will be an extension identifier' + ] + }, "Activated by {0} on start-up", activationId); } else if (/^workspaceContains:/.test(activationEvent)) { let fileNameOrGlob = activationEvent.substr('workspaceContains:'.length); if (fileNameOrGlob.indexOf('*') >= 0 || fileNameOrGlob.indexOf('?') >= 0) { title = nls.localize({ key: 'workspaceContainsGlobActivation', comment: [ - '{0} will be a glob pattern' + '{0} will be a glob pattern', + '{1} will be an extension identifier' ] - }, "Activated by {1} because a file matching {1} exists in your workspace", fileNameOrGlob, activationId); + }, "Activated by {1} because a file matching {0} exists in your workspace", fileNameOrGlob, activationId); } else { title = nls.localize({ key: 'workspaceContainsFileActivation', comment: [ - '{0} will be a file name' + '{0} will be a file name', + '{1} will be an extension identifier' ] }, "Activated by {1} because file {0} exists in your workspace", fileNameOrGlob, activationId); } @@ -320,7 +327,8 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { title = nls.localize({ key: 'workspaceContainsTimeout', comment: [ - '{0} will be a glob pattern' + '{0} will be a glob pattern', + '{1} will be an extension identifier' ] }, "Activated by {1} because searching for {0} took too long", glob, activationId); } else if (activationEvent === 'onStartupFinished') { @@ -337,7 +345,8 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { title = nls.localize({ key: 'workspaceGenericActivation', comment: [ - 'The {0} placeholder will be an activation event, like e.g. \'language:typescript\', \'debug\', etc.' + '{0} will be an activation event, like e.g. \'language:typescript\', \'debug\', etc.', + '{1} will be an extension identifier' ] }, "Activated by {1} on {0}", activationEvent, activationId); } @@ -346,28 +355,28 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { clearNode(data.msgContainer); if (this._getUnresponsiveProfile(element.description.identifier)) { - const el = $('span', undefined, ...renderCodicons(` $(alert) Unresponsive`)); + const el = $('span', undefined, ...renderLabelWithIcons(` $(alert) Unresponsive`)); el.title = nls.localize('unresponsive.title', "Extension has caused the extension host to freeze."); data.msgContainer.appendChild(el); } if (isNonEmptyArray(element.status.runtimeErrors)) { - const el = $('span', undefined, ...renderCodicons(`$(bug) ${nls.localize('errors', "{0} uncaught errors", element.status.runtimeErrors.length)}`)); + const el = $('span', undefined, ...renderLabelWithIcons(`$(bug) ${nls.localize('errors', "{0} uncaught errors", element.status.runtimeErrors.length)}`)); data.msgContainer.appendChild(el); } if (element.status.messages && element.status.messages.length > 0) { - const el = $('span', undefined, ...renderCodicons(`$(alert) ${element.status.messages[0].message}`)); + const el = $('span', undefined, ...renderLabelWithIcons(`$(alert) ${element.status.messages[0].message}`)); data.msgContainer.appendChild(el); } if (element.description.extensionLocation.scheme === Schemas.vscodeRemote) { - const el = $('span', undefined, ...renderCodicons(`$(remote) ${element.description.extensionLocation.authority}`)); + const el = $('span', undefined, ...renderLabelWithIcons(`$(remote) ${element.description.extensionLocation.authority}`)); data.msgContainer.appendChild(el); const hostLabel = this._labelService.getHostLabel(Schemas.vscodeRemote, this._environmentService.remoteAuthority); if (hostLabel) { - reset(el, ...renderCodicons(`$(remote) ${hostLabel}`)); + reset(el, ...renderLabelWithIcons(`$(remote) ${hostLabel}`)); } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 7a58b96d..e7f0a15b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -65,22 +65,14 @@ import { generateTokensCSSForColorMap } from 'vs/editor/common/modes/supports/to import { editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { insane } from 'vs/base/common/insane/insane'; function removeEmbeddedSVGs(documentContent: string): string { - const newDocument = new DOMParser().parseFromString(documentContent, 'text/html'); - - // remove all inline svgs - const allSVGs = newDocument.documentElement.querySelectorAll('svg'); - if (allSVGs) { - for (let i = 0; i < allSVGs.length; i++) { - const svg = allSVGs[i]; - if (svg.parentNode) { - svg.parentNode.removeChild(allSVGs[i]); - } + return insane(documentContent, { + filter(token: { tag: string, attrs: { readonly [key: string]: string } }): boolean { + return token.tag !== 'svg'; } - } - - return newDocument.documentElement.outerHTML; + }); } class NavBar extends Disposable { @@ -169,6 +161,11 @@ interface IExtensionEditorTemplate { header: HTMLElement; } +const enum WebviewIndex { + Readme, + Changelog +} + export class ExtensionEditor extends EditorPane { static readonly ID: string = 'workbench.editor.extension'; @@ -179,6 +176,12 @@ export class ExtensionEditor extends EditorPane { private extensionChangelog: Cache | null; private extensionManifest: Cache | null; + // Some action bar items use a webview whose vertical scroll position we track in this map + private initialScrollProgress: Map = new Map(); + + // Spot when an ExtensionEditor instance gets reused for a different extension, in which case the vertical scroll positions must be zeroed + private currentIdentifier: string = ''; + private layoutParticipants: ILayoutParticipant[] = []; private readonly contentDisposables = this._register(new DisposableStore()); private readonly transientDisposables = this._register(new DisposableStore()); @@ -336,6 +339,11 @@ export class ExtensionEditor extends EditorPane { this.editorLoadComplete = false; const extension = input.extension; + if (this.currentIdentifier !== extension.identifier.id) { + this.initialScrollProgress.clear(); + this.currentIdentifier = extension.identifier.id; + } + this.transientDisposables.clear(); this.extensionReadme = new Cache(() => createCancelablePromise(token => extension.getReadme(token))); @@ -563,7 +571,7 @@ export class ExtensionEditor extends EditorPane { return Promise.resolve(null); } - private async openMarkdown(cacheResult: CacheResult, noContentCopy: string, template: IExtensionEditorTemplate, token: CancellationToken): Promise { + private async openMarkdown(cacheResult: CacheResult, noContentCopy: string, template: IExtensionEditorTemplate, webviewIndex: WebviewIndex, token: CancellationToken): Promise { try { const body = await this.renderMarkdown(cacheResult, template); if (token.isCancellationRequested) { @@ -572,15 +580,22 @@ export class ExtensionEditor extends EditorPane { const webview = this.contentDisposables.add(this.webviewService.createWebviewOverlay('extensionEditor', { enableFindWidget: true, + tryRestoreScrollPosition: true, }, {}, undefined)); + webview.initialScrollProgress = this.initialScrollProgress.get(webviewIndex) || 0; + webview.claim(this, this.scopedContextKeyService); setParentFlowTo(webview.container, template.content); webview.layoutWebviewOverElement(template.content); webview.html = body; + webview.claim(this, undefined); this.contentDisposables.add(webview.onDidFocus(() => this.fireOnDidFocus())); + + this.contentDisposables.add(webview.onDidScroll(() => this.initialScrollProgress.set(webviewIndex, webview.initialScrollProgress))); + const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout: () => { webview.layoutWebviewOverElement(template.content); @@ -622,8 +637,8 @@ export class ExtensionEditor extends EditorPane { private async renderMarkdown(cacheResult: CacheResult, template: IExtensionEditorTemplate) { const contents = await this.loadContents(() => cacheResult, template); const content = await renderMarkdownDocument(contents, this.extensionService, this.modeService); - const documentContent = await this.renderBody(content); - return removeEmbeddedSVGs(documentContent); + const sanitizedContent = removeEmbeddedSVGs(content); + return await this.renderBody(sanitizedContent); } private async renderBody(body: string): Promise { @@ -709,9 +724,16 @@ export class ExtensionEditor extends EditorPane { } code { + font-family: "SF Mono", Monaco, Menlo, Consolas, "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace; + font-size: 14px; + line-height: 19px; + } + + pre code { font-family: var(--vscode-editor-font-family); font-weight: var(--vscode-editor-font-weight); font-size: var(--vscode-editor-font-size); + line-height: 1.5; } code > div { @@ -823,7 +845,7 @@ export class ExtensionEditor extends EditorPane { if (manifest && manifest.extensionPack && manifest.extensionPack.length) { return this.openExtensionPackReadme(manifest, template, token); } - return this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available."), template, token); + return this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available."), template, WebviewIndex.Readme, token); } private async openExtensionPackReadme(manifest: IExtensionManifest, template: IExtensionEditorTemplate, token: CancellationToken): Promise { @@ -855,14 +877,14 @@ export class ExtensionEditor extends EditorPane { await Promise.all([ this.renderExtensionPack(manifest, extensionPackContent, token), - this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available."), { ...template, ...{ content: readmeContent } }, token), + this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available."), { ...template, ...{ content: readmeContent } }, WebviewIndex.Readme, token), ]); return { focus: () => extensionPackContent.focus() }; } private openChangelog(template: IExtensionEditorTemplate, token: CancellationToken): Promise { - return this.openMarkdown(this.extensionChangelog!.get(), localize('noChangelog', "No Changelog available."), template, token); + return this.openMarkdown(this.extensionChangelog!.get(), localize('noChangelog', "No Changelog available."), template, WebviewIndex.Changelog, token); } private openContributions(template: IExtensionEditorTemplate, token: CancellationToken): Promise { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts index ab902f41..d48a5cde 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts @@ -13,7 +13,7 @@ import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; +import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource, RecommendationSourceToString } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; import { INotificationHandle, INotificationService, IPromptChoice, IPromptChoiceWithMenu, Severity } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -21,6 +21,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUserDataAutoSyncEnablementService, IUserDataSyncResourceEnablementService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { SearchExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; import { EnablementState, IWorkbenchExtensioManagementService, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionIgnoredRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; @@ -28,6 +29,7 @@ import { IExtensionIgnoredRecommendationsService } from 'vs/workbench/services/e type ExtensionRecommendationsNotificationClassification = { userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; extensionId?: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' }; + source: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; }; type ExtensionWorkspaceRecommendationsNotificationClassification = { @@ -135,6 +137,7 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec @IExtensionIgnoredRecommendationsService private readonly extensionIgnoredRecommendationsService: IExtensionIgnoredRecommendationsService, @IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService, @IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, + @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, @optional(ITASExperimentService) tasExperimentService: ITASExperimentService, ) { this.tasExperimentService = tasExperimentService; @@ -153,13 +156,13 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec } return this.promptRecommendationsNotification(extensionIds, message, searchValue, source, { - onDidInstallRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId: extension.identifier.id })), - onDidShowRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId: extension.identifier.id })), - onDidCancelRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId: extension.identifier.id })), + onDidInstallRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string, extensionId: string, source: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId: extension.identifier.id, source: RecommendationSourceToString(source) })), + onDidShowRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string, extensionId: string, source: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId: extension.identifier.id, source: RecommendationSourceToString(source) })), + onDidCancelRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string, extensionId: string, source: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId: extension.identifier.id, source: RecommendationSourceToString(source) })), onDidNeverShowRecommendedExtensionsAgain: (extensions: IExtension[]) => { for (const extension of extensions) { this.addToImportantRecommendationsIgnore(extension.identifier.id); - this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: extension.identifier.id }); + this.telemetryService.publicLog2<{ userReaction: string, extensionId: string, source: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: extension.identifier.id, source: RecommendationSourceToString(source) }); } this.notificationService.prompt( Severity.Info, @@ -207,6 +210,11 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec return RecommendationsNotificationResult.Ignored; } + // Do not show exe based recommendations in remote window + if (source === RecommendationSource.EXE && this.workbenchEnvironmentService.remoteAuthority) { + return RecommendationsNotificationResult.IncompatibleWindow; + } + // Ignore exe recommendation if the window // => has shown an exe based recommendation already // => or has shown any two recommendations already diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 839c3ffb..4cb6024b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -6,20 +6,16 @@ import { localize } from 'vs/nls'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/registry/common/platform'; -import { MenuRegistry, MenuId, registerAction2, Action2, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { MenuRegistry, MenuId, registerAction2, Action2, SyncActionDescriptor, ISubmenuItem, IMenuItem, IAction2Options } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ExtensionsLabel, ExtensionsLocalizedLabel, ExtensionsChannelId, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { ExtensionsLabel, ExtensionsLocalizedLabel, ExtensionsChannelId, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/services/output/common/output'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; -import { - OpenExtensionsViewletAction, InstallExtensionsAction, ShowOutdatedExtensionsAction, ShowRecommendedExtensionsAction, ShowRecommendedKeymapExtensionsAction, ShowPopularExtensionsAction, - ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowDisabledExtensionsAction, ShowBuiltInExtensionsAction, UpdateAllAction, - EnableAllAction, EnableAllWorkspaceAction, DisableAllAction, DisableAllWorkspaceAction, CheckForUpdatesAction, ShowLanguageExtensionsAction, EnableAutoUpdateAction, DisableAutoUpdateAction, InstallVSIXAction, ReinstallAction, InstallSpecificVersionOfExtensionAction, ClearExtensionsSearchResultsAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, RefreshExtensionsAction -} from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, DefaultViewsContext, ExtensionsSortByContext, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; +import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, PromptExtensionInstallFailureAction, SearchExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor'; import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContribution, ExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/browser/extensionsViewlet'; @@ -37,11 +33,10 @@ import { ExtensionActivationProgress } from 'vs/workbench/contrib/extensions/bro import { onUnexpectedError } from 'vs/base/common/errors'; import { ExtensionDependencyChecker } from 'vs/workbench/contrib/extensions/browser/extensionsDependencyChecker'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { RemoteExtensionsInstaller } from 'vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller'; -import { IViewContainersRegistry, ViewContainerLocation, Extensions as ViewContainerExtensions } from 'vs/workbench/common/views'; +import { IViewContainersRegistry, ViewContainerLocation, Extensions as ViewContainerExtensions, IViewsService } from 'vs/workbench/common/views'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; -import { ContextKeyAndExpr, ContextKeyExpr, ContextKeyOrExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyAndExpr, ContextKeyDefinedExpr, ContextKeyEqualsExpr, ContextKeyExpr, ContextKeyOrExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess'; import { InstallExtensionQuickAccessProvider, ManageExtensionsQuickAccessProvider } from 'vs/workbench/contrib/extensions/browser/extensionsQuickAccess'; @@ -65,7 +60,15 @@ import { IAction } from 'vs/base/common/actions'; import { IWorkpsaceExtensionsConfigService } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig'; import { Schemas } from 'vs/base/common/network'; import { ShowRuntimeExtensionsAction } from 'vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor'; -import { extensionsViewIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; +import { clearSearchResultsIcon, configureRecommendedIcon, extensionsViewIcon, filterIcon, installWorkspaceRecommendedIcon, refreshIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; +import { EXTENSION_CATEGORIES } from 'vs/platform/extensions/common/extensions'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { isArray } from 'vs/base/common/types'; +import { ShowViewletAction } from 'vs/workbench/browser/viewlet'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { mnemonicButtonLabel } from 'vs/base/common/labels'; +import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); @@ -83,16 +86,6 @@ Registry.as(Extensions.Quickaccess).registerQuickAccessPro helpEntries: [{ description: localize('manageExtensionsHelp', "Manage Extensions"), needsEditor: false }] }); -// Explorer -MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { - group: 'extensions', - command: { - id: INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, - title: localize('installVSIX', "Install Extension VSIX"), - }, - when: ResourceContextKey.Extension.isEqualTo('.vsix') -}); - // Editor Registry.as(EditorExtensions.Editors).registerEditor( EditorDescriptor.create( @@ -226,36 +219,6 @@ CommandsRegistry.registerCommand({ } }); -CommandsRegistry.registerCommand({ - id: INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, - handler: async (accessor: ServicesAccessor, resources: URI[] | URI) => { - const extensionService = accessor.get(IExtensionService); - const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); - const hostService = accessor.get(IHostService); - const notificationService = accessor.get(INotificationService); - - const extensions = Array.isArray(resources) ? resources : [resources]; - await Promise.all(extensions.map(async (vsix) => await extensionsWorkbenchService.install(vsix))) - .then(async (extensions) => { - for (const extension of extensions) { - const requireReload = !(extension.local && extensionService.canAddExtension(toExtensionDescription(extension.local))); - const message = requireReload ? localize('InstallVSIXAction.successReload', "Completed installing {0} extension from VSIX. Please reload Visual Studio Code to enable it.", extension.displayName || extension.name) - : localize('InstallVSIXAction.success', "Completed installing {0} extension from VSIX.", extension.displayName || extension.name); - const actions = requireReload ? [{ - label: localize('InstallVSIXAction.reloadNow', "Reload Now"), - run: () => hostService.reload() - }] : []; - notificationService.prompt( - Severity.Info, - message, - actions, - { sticky: true } - ); - } - }); - } -}); - CommandsRegistry.registerCommand({ id: 'workbench.extensions.uninstallExtension', description: { @@ -316,57 +279,6 @@ CommandsRegistry.registerCommand({ } }); -// File menu registration - -MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - group: '2_keybindings', - command: { - id: ShowRecommendedKeymapExtensionsAction.ID, - title: localize({ key: 'miOpenKeymapExtensions', comment: ['&& denotes a mnemonic'] }, "&&Keymaps") - }, - order: 2 -}); - -MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { - group: '2_keybindings', - command: { - id: ShowRecommendedKeymapExtensionsAction.ID, - title: localize('miOpenKeymapExtensions2', "Keymaps") - }, - order: 2 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - group: '1_settings', - command: { - id: VIEWLET_ID, - title: localize({ key: 'miPreferencesExtensions', comment: ['&& denotes a mnemonic'] }, "&&Extensions") - }, - order: 3 -}); - -// View menu - -MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '3_views', - command: { - id: VIEWLET_ID, - title: localize({ key: 'miViewExtensions', comment: ['&& denotes a mnemonic'] }, "E&&xtensions") - }, - order: 5 -}); - -// Global Activity Menu - -MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { - group: '2_configuration', - command: { - id: VIEWLET_ID, - title: localize('showExtensions', "Extensions") - }, - order: 3 -}); - function overrideActionForActiveExtensionEditorWebview(command: MultiCommand | undefined, f: (webview: Webview) => void) { command?.addImplementation(105, (accessor) => { const editorService = accessor.get(IEditorService); @@ -399,13 +311,25 @@ async function runAction(action: IAction): Promise { } } -class ExtensionsContributions implements IWorkbenchContribution { +interface IExtensionActionOptions extends IAction2Options { + menuTitles?: { [id: number]: string }; + run(accessor: ServicesAccessor, ...args: any[]): Promise; +} + +class ExtensionsContributions extends Disposable implements IWorkbenchContribution { constructor( @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, @IContextKeyService contextKeyService: IContextKeyService, + @IViewletService private readonly viewletService: IViewletService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @INotificationService private readonly notificationService: INotificationService, + @ICommandService private readonly commandService: ICommandService, ) { + super(); const hasGalleryContext = CONTEXT_HAS_GALLERY.bindTo(contextKeyService); if (extensionGalleryService.isEnabled()) { hasGalleryContext.set(true); @@ -447,437 +371,667 @@ class ExtensionsContributions implements IWorkbenchContribution { // Global actions private registerGlobalActions(): void { - registerAction2(class extends Action2 { - constructor() { - super({ - id: OpenExtensionsViewletAction.ID, - title: { value: OpenExtensionsViewletAction.LABEL, original: 'Show Extensions' }, - category: CATEGORIES.View, - menu: { - id: MenuId.CommandPalette, - }, - keybinding: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_X, - weight: KeybindingWeight.WorkbenchContrib + this.registerExtensionAction({ + id: VIEWLET_ID, + title: { value: localize('toggleExtensionsViewlet', "Show Extensions"), original: 'Show Extensions' }, + category: CATEGORIES.View, + menu: [{ + id: MenuId.CommandPalette, + }, { + id: MenuId.MenubarPreferencesMenu, + group: '1_settings', + order: 3 + }, { + id: MenuId.MenubarViewMenu, + group: '3_views', + order: 5 + }, { + id: MenuId.GlobalActivity, + group: '2_configuration', + order: 3 + }], + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_X, + weight: KeybindingWeight.WorkbenchContrib + }, + menuTitles: { + [MenuId.MenubarPreferencesMenu.id]: localize({ key: 'miPreferencesExtensions', comment: ['&& denotes a mnemonic'] }, "&&Extensions"), + [MenuId.MenubarViewMenu.id]: localize({ key: 'miViewExtensions', comment: ['&& denotes a mnemonic'] }, "E&&xtensions"), + [MenuId.GlobalActivity.id]: localize('showExtensions', "Extensions"), + }, + run: () => runAction(this.instantiationService.createInstance(ShowViewletAction, VIEWLET_ID, 'Show Extensions', VIEWLET_ID)) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.installExtensions', + title: { value: localize('installExtensions', "Install Extensions"), original: 'Install Extensions' }, + category: ExtensionsLocalizedLabel, + menu: { + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + }, + run: () => runAction(this.instantiationService.createInstance(ShowViewletAction, VIEWLET_ID, 'Install Extensions', VIEWLET_ID)) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.showRecommendedKeymapExtensions', + title: { value: localize('showRecommendedKeymapExtensionsShort', "Keymaps"), original: 'Keymaps' }, + category: PreferencesLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: CONTEXT_HAS_GALLERY + }, { + id: MenuId.MenubarPreferencesMenu, + group: '2_keybindings', + order: 2 + }, { + id: MenuId.GlobalActivity, + group: '2_keybindings', + order: 2 + }], + keybinding: { + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_M), + weight: KeybindingWeight.WorkbenchContrib + }, + menuTitles: { + [MenuId.MenubarPreferencesMenu.id]: localize({ key: 'miOpenKeymapExtensions', comment: ['&& denotes a mnemonic'] }, "&&Keymaps"), + [MenuId.GlobalActivity.id]: localize('miOpenKeymapExtensions2', "Keymaps") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@recommended:keymaps ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.showLanguageExtensions', + title: { value: localize('showLanguageExtensionsShort', "Language Extensions"), original: 'Language Extensions' }, + category: PreferencesLocalizedLabel, + menu: { + id: MenuId.CommandPalette, + when: CONTEXT_HAS_GALLERY + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@category:"programming languages" @sort:installs ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.checkForUpdates', + title: { value: localize('checkForUpdates', "Check for Extension Updates"), original: 'Check for Extension Updates' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + }, { + id: MenuId.ViewContainerTitle, + when: ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), + group: '1_updates', + order: 1 + }], + run: async () => { + await this.extensionsWorkbenchService.checkForUpdates(); + const outdated = this.extensionsWorkbenchService.outdated; + if (!outdated.length) { + this.notificationService.info(localize('noUpdatesAvailable', "All extensions are up to date.")); + return; + } + + let msgAvailableExtensions = outdated.length === 1 ? localize('singleUpdateAvailable', "An extension update is available.") : localize('updatesAvailable', "{0} extension updates are available.", outdated.length); + + const disabledExtensionsCount = outdated.filter(ext => ext.local && !this.extensionEnablementService.isEnabled(ext.local)).length; + if (disabledExtensionsCount) { + if (outdated.length === 1) { + msgAvailableExtensions = localize('singleDisabledUpdateAvailable', "An update to an extension which is disabled is available."); + } else if (disabledExtensionsCount === 1) { + msgAvailableExtensions = localize('updatesAvailableOneDisabled', "{0} extension updates are available. One of them is for a disabled extension.", outdated.length); + } else if (disabledExtensionsCount === outdated.length) { + msgAvailableExtensions = localize('updatesAvailableAllDisabled', "{0} extension updates are available. All of them are for disabled extensions.", outdated.length); + } else { + msgAvailableExtensions = localize('updatesAvailableIncludingDisabled', "{0} extension updates are available. {1} of them are for disabled extensions.", outdated.length, disabledExtensionsCount); } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(OpenExtensionsViewletAction, OpenExtensionsViewletAction.ID, OpenExtensionsViewletAction.LABEL)); + } + + this.viewletService.openViewlet(VIEWLET_ID, true); + this.notificationService.info(msgAvailableExtensions); } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: InstallExtensionsAction.ID, - title: { value: InstallExtensionsAction.LABEL, original: 'Install Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + this.registerExtensionAction({ + id: 'workbench.extensions.action.disableAutoUpdate', + title: { value: localize('disableAutoUpdate', "Disable Auto Updating Extensions"), original: 'Disable Auto Updating Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + }, { + id: MenuId.ViewContainerTitle, + when: ContextKeyAndExpr.create([ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), ContextKeyDefinedExpr.create(`config.${AutoUpdateConfigurationKey}`)]), + group: '1_updates', + order: 2 + }], + run: (accessor: ServicesAccessor) => accessor.get(IConfigurationService).updateValue(AutoUpdateConfigurationKey, false) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.updateAllExtensions', + title: { value: localize('updateAll', "Update All Extensions"), original: 'Update All Extensions' }, + category: ExtensionsLocalizedLabel, + precondition: HasOutdatedExtensionsContext, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + }, { + id: MenuId.ViewContainerTitle, + when: ContextKeyAndExpr.create([ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), ContextKeyDefinedExpr.create(`config.${AutoUpdateConfigurationKey}`).negate()]), + group: '1_updates', + order: 2 + }], + run: () => { + return Promise.all(this.extensionsWorkbenchService.outdated.map(async extension => { + try { + await this.extensionsWorkbenchService.install(extension); + } catch (err) { + runAction(this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Update, err)); } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(InstallExtensionsAction, InstallExtensionsAction.ID, InstallExtensionsAction.LABEL)); + })); } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowOutdatedExtensionsAction.ID, - title: { value: ShowOutdatedExtensionsAction.LABEL, original: 'Show Outdated Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowOutdatedExtensionsAction, ShowOutdatedExtensionsAction.ID, ShowOutdatedExtensionsAction.LABEL)); + this.registerExtensionAction({ + id: 'workbench.extensions.action.enableAutoUpdate', + title: { value: localize('enableAutoUpdate', "Enable Auto Updating Extensions"), original: 'Enable Auto Updating Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + }, { + id: MenuId.ViewContainerTitle, + when: ContextKeyAndExpr.create([ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), ContextKeyDefinedExpr.create(`config.${AutoUpdateConfigurationKey}`).negate()]), + group: '1_updates', + order: 3 + }], + run: (accessor: ServicesAccessor) => accessor.get(IConfigurationService).updateValue(AutoUpdateConfigurationKey, true) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.enableAll', + title: { value: localize('enableAll', "Enable All Extensions"), original: 'Enable All Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) + }, { + id: MenuId.ViewContainerTitle, + when: ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), + group: '2_enablement', + order: 1 + }], + run: async () => { + const extensionsToEnable = this.extensionsWorkbenchService.local.filter(e => !!e.local && this.extensionEnablementService.canChangeEnablement(e.local) && !this.extensionEnablementService.isEnabled(e.local)); + if (extensionsToEnable.length) { + await this.extensionsWorkbenchService.setEnablement(extensionsToEnable, EnablementState.EnabledGlobally); + } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowRecommendedExtensionsAction.ID, - title: { value: ShowRecommendedExtensionsAction.LABEL, original: 'Show Recommended Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: CONTEXT_HAS_GALLERY - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, ShowRecommendedExtensionsAction.LABEL)); + this.registerExtensionAction({ + id: 'workbench.extensions.action.enableAllWorkspace', + title: { value: localize('enableAllWorkspace', "Enable All Extensions for this Workspace"), original: 'Enable All Extensions for this Workspace' }, + category: ExtensionsLocalizedLabel, + menu: { + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([WorkbenchStateContext.notEqualsTo('empty'), ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + }, + run: async () => { + const extensionsToEnable = this.extensionsWorkbenchService.local.filter(e => !!e.local && this.extensionEnablementService.canChangeEnablement(e.local) && !this.extensionEnablementService.isEnabled(e.local)); + if (extensionsToEnable.length) { + await this.extensionsWorkbenchService.setEnablement(extensionsToEnable, EnablementState.EnabledWorkspace); + } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowRecommendedKeymapExtensionsAction.ID, - title: { value: ShowRecommendedKeymapExtensionsAction.LABEL, original: 'Keymaps' }, - category: PreferencesLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: CONTEXT_HAS_GALLERY - }, - keybinding: { - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_M), - weight: KeybindingWeight.WorkbenchContrib - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowRecommendedKeymapExtensionsAction, ShowRecommendedKeymapExtensionsAction.ID, ShowRecommendedKeymapExtensionsAction.LABEL)); + this.registerExtensionAction({ + id: 'workbench.extensions.action.disableAll', + title: { value: localize('disableAll', "Disable All Installed Extensions"), original: 'Disable All Installed Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) + }, { + id: MenuId.ViewContainerTitle, + when: ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), + group: '2_enablement', + order: 2 + }], + run: async () => { + const extensionsToDisable = this.extensionsWorkbenchService.local.filter(e => !e.isBuiltin && !!e.local && this.extensionEnablementService.isEnabled(e.local) && this.extensionEnablementService.canChangeEnablement(e.local)); + if (extensionsToDisable.length) { + await this.extensionsWorkbenchService.setEnablement(extensionsToDisable, EnablementState.DisabledGlobally); + } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowLanguageExtensionsAction.ID, - title: { value: ShowLanguageExtensionsAction.LABEL, original: 'Language Extensions' }, - category: PreferencesLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: CONTEXT_HAS_GALLERY - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowLanguageExtensionsAction, ShowLanguageExtensionsAction.ID, ShowLanguageExtensionsAction.LABEL)); + this.registerExtensionAction({ + id: 'workbench.extensions.action.disableAllWorkspace', + title: { value: localize('disableAllWorkspace', "Disable All Installed Extensions for this Workspace"), original: 'Disable All Installed Extensions for this Workspace' }, + category: ExtensionsLocalizedLabel, + menu: { + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([WorkbenchStateContext.notEqualsTo('empty'), ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + }, + run: async () => { + const extensionsToDisable = this.extensionsWorkbenchService.local.filter(e => !e.isBuiltin && !!e.local && this.extensionEnablementService.isEnabled(e.local) && this.extensionEnablementService.canChangeEnablement(e.local)); + if (extensionsToDisable.length) { + await this.extensionsWorkbenchService.setEnablement(extensionsToDisable, EnablementState.DisabledWorkspace); + } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowPopularExtensionsAction.ID, - title: { value: ShowPopularExtensionsAction.LABEL, original: 'Show Popular Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: CONTEXT_HAS_GALLERY - } + this.registerExtensionAction({ + id: SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, + title: { value: localize('InstallFromVSIX', "Install from VSIX..."), original: 'Install from VSIX...' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER]) + }, { + id: MenuId.ViewContainerTitle, + when: ContextKeyAndExpr.create([ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER])]), + group: '3_install', + order: 1 + }], + run: async (accessor: ServicesAccessor) => { + const fileDialogService = accessor.get(IFileDialogService); + const commandService = accessor.get(ICommandService); + const vsixPaths = await fileDialogService.showOpenDialog({ + title: localize('installFromVSIX', "Install from VSIX"), + filters: [{ name: 'VSIX Extensions', extensions: ['vsix'] }], + canSelectFiles: true, + canSelectMany: true, + openLabel: mnemonicButtonLabel(localize({ key: 'installButton', comment: ['&& denotes a mnemonic'] }, "&&Install")) }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowPopularExtensionsAction, ShowPopularExtensionsAction.ID, ShowPopularExtensionsAction.LABEL)); + if (vsixPaths) { + await commandService.executeCommand(INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, vsixPaths); + } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowEnabledExtensionsAction.ID, - title: { value: ShowEnabledExtensionsAction.LABEL, original: 'Show Enabled Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowEnabledExtensionsAction, ShowEnabledExtensionsAction.ID, ShowEnabledExtensionsAction.LABEL)); + this.registerExtensionAction({ + id: INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, + title: localize('installVSIX', "Install Extension VSIX"), + menu: [{ + id: MenuId.ExplorerContext, + group: 'extensions', + when: ContextKeyAndExpr.create([ResourceContextKey.Extension.isEqualTo('.vsix'), ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER])]), + }], + run: async (accessor: ServicesAccessor, resources: URI[] | URI) => { + const extensionService = accessor.get(IExtensionService); + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); + const hostService = accessor.get(IHostService); + const notificationService = accessor.get(INotificationService); + + const extensions = Array.isArray(resources) ? resources : [resources]; + await Promise.all(extensions.map(async (vsix) => await extensionsWorkbenchService.install(vsix))) + .then(async (extensions) => { + for (const extension of extensions) { + const requireReload = !(extension.local && extensionService.canAddExtension(toExtensionDescription(extension.local))); + const message = requireReload ? localize('InstallVSIXAction.successReload', "Completed installing {0} extension from VSIX. Please reload Visual Studio Code to enable it.", extension.displayName || extension.name) + : localize('InstallVSIXAction.success', "Completed installing {0} extension from VSIX.", extension.displayName || extension.name); + const actions = requireReload ? [{ + label: localize('InstallVSIXAction.reloadNow', "Reload Now"), + run: () => hostService.reload() + }] : []; + notificationService.prompt( + Severity.Info, + message, + actions, + { sticky: true } + ); + } + }); } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowInstalledExtensionsAction.ID, - title: { value: ShowInstalledExtensionsAction.LABEL, original: 'Show Installed Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, ShowInstalledExtensionsAction.LABEL)); + const extensionsFilterSubMenu = new MenuId('extensionsFilterSubMenu'); + MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { + submenu: extensionsFilterSubMenu, + title: localize('filterExtensions', "Filter Extensions..."), + when: ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), + group: 'navigation', + order: 1, + icon: filterIcon, + }); + + const showFeaturedExtensionsId = 'extensions.filter.featured'; + this.registerExtensionAction({ + id: showFeaturedExtensionsId, + title: { value: localize('showFeaturedExtensions', "Show Featured Extensions"), original: 'Show Featured Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: CONTEXT_HAS_GALLERY + }, { + id: extensionsFilterSubMenu, + when: CONTEXT_HAS_GALLERY, + group: '1_predefined', + order: 1, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('featured filter', "Featured") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@featured ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.showPopularExtensions', + title: { value: localize('showPopularExtensions', "Show Popular Extensions"), original: 'Show Popular Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: CONTEXT_HAS_GALLERY + }, { + id: extensionsFilterSubMenu, + when: CONTEXT_HAS_GALLERY, + group: '1_predefined', + order: 2, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('most popular filter', "Most Popular") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@popular ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.showRecommendedExtensions', + title: { value: localize('showRecommendedExtensions', "Show Recommended Extensions"), original: 'Show Recommended Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: CONTEXT_HAS_GALLERY + }, { + id: extensionsFilterSubMenu, + when: CONTEXT_HAS_GALLERY, + group: '1_predefined', + order: 2, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('most popular recommended', "Recommended") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@recommended ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.recentlyPublishedExtensions', + title: { value: localize('recentlyPublishedExtensions', "Show Recently Published Extensions"), original: 'Show Recently Published Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: CONTEXT_HAS_GALLERY + }, { + id: extensionsFilterSubMenu, + when: CONTEXT_HAS_GALLERY, + group: '1_predefined', + order: 2, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('recently published filter', "Recently Published") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@sort:publishedDate ')) + }); + + const extensionsCategoryFilterSubMenu = new MenuId('extensionsCategoryFilterSubMenu'); + MenuRegistry.appendMenuItem(extensionsFilterSubMenu, { + submenu: extensionsCategoryFilterSubMenu, + title: localize('filter by category', "Category"), + when: CONTEXT_HAS_GALLERY, + group: '2_categories', + order: 1, + }); + + EXTENSION_CATEGORIES.map((category, index) => { + this.registerExtensionAction({ + id: `extensions.actions.searchByCategory.${category}`, + title: category, + menu: [{ + id: extensionsCategoryFilterSubMenu, + when: CONTEXT_HAS_GALLERY, + order: index, + }], + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, `@category:"${category.toLowerCase()}"`)) + }); + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.listBuiltInExtensions', + title: { value: localize('showBuiltInExtensions', "Show Built-in Extensions"), original: 'Show Built-in Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) + }, { + id: extensionsFilterSubMenu, + group: '3_installed', + order: 1, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('builtin filter', "Built-in") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@builtin ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.showInstalledExtensions', + title: { value: localize('showInstalledExtensions', "Show Installed Extensions"), original: 'Show Installed Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) + }, { + id: extensionsFilterSubMenu, + group: '3_installed', + order: 2, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('installed filter', "Installed") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@installed ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.showEnabledExtensions', + title: { value: localize('showEnabledExtensions', "Show Enabled Extensions"), original: 'Show Enabled Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) + }, { + id: extensionsFilterSubMenu, + group: '3_installed', + order: 3, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('enabled filter', "Enabled") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@enabled ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.showDisabledExtensions', + title: { value: localize('showDisabledExtensions', "Show Disabled Extensions"), original: 'Show Disabled Extensions' }, + category: ExtensionsLocalizedLabel, + + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) + }, { + id: extensionsFilterSubMenu, + group: '3_installed', + order: 4, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('disabled filter', "Disabled") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@disabled ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.listOutdatedExtensions', + title: { value: localize('showOutdatedExtensions', "Show Outdated Extensions"), original: 'Show Outdated Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + }, { + id: extensionsFilterSubMenu, + group: '3_installed', + order: 5, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('outdated filter', "Outdated") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@outdated ')) + }); + + const extensionsSortSubMenu = new MenuId('extensionsSortSubMenu'); + MenuRegistry.appendMenuItem(extensionsFilterSubMenu, { + submenu: extensionsSortSubMenu, + title: localize('sorty by', "Sort By"), + when: CONTEXT_HAS_GALLERY, + group: '4_sort', + order: 1, + }); + + [ + { id: 'installs', title: localize('sort by installs', "Install Count") }, + { id: 'rating', title: localize('sort by rating', "Rating") }, + { id: 'name', title: localize('sort by name', "Name") }, + { id: 'publishedDate', title: localize('sort by date', "Published Date") }, + ].map(({ id, title }, index) => { + this.registerExtensionAction({ + id: `extensions.sort.${id}`, + title, + precondition: DefaultViewsContext.toNegated(), + menu: [{ + id: extensionsSortSubMenu, + when: CONTEXT_HAS_GALLERY, + order: index, + }], + toggled: ExtensionsSortByContext.isEqualTo(id), + run: async () => { + const viewlet = await this.viewletService.openViewlet(VIEWLET_ID, true); + const extensionsViewPaneContainer = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer; + const currentQuery = Query.parse(extensionsViewPaneContainer.searchValue || ''); + extensionsViewPaneContainer.search(new Query(currentQuery.value, id, currentQuery.groupBy).toString()); + extensionsViewPaneContainer.focus(); + } + }); + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.clearExtensionsSearchResults', + title: { value: localize('clearExtensionsSearchResults', "Clear Extensions Search Results"), original: 'Clear Extensions Search Results' }, + category: ExtensionsLocalizedLabel, + icon: clearSearchResultsIcon, + f1: true, + precondition: DefaultViewsContext.toNegated(), + menu: { + id: MenuId.ViewContainerTitle, + when: ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), + group: 'navigation', + order: 3, + }, + run: async (accessor: ServicesAccessor) => { + const viewPaneContainer = accessor.get(IViewsService).getActiveViewPaneContainerWithId(VIEWLET_ID); + if (viewPaneContainer) { + const extensionsViewPaneContainer = viewPaneContainer as IExtensionsViewPaneContainer; + extensionsViewPaneContainer.search(''); + extensionsViewPaneContainer.focus(); + } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowDisabledExtensionsAction.ID, - title: { value: ShowDisabledExtensionsAction.LABEL, original: 'Show Disabled Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowDisabledExtensionsAction, ShowDisabledExtensionsAction.ID, ShowDisabledExtensionsAction.LABEL)); + this.registerExtensionAction({ + id: 'workbench.extensions.action.refreshExtension', + title: { value: localize('refreshExtension', "Refresh"), original: 'Refresh' }, + category: ExtensionsLocalizedLabel, + icon: refreshIcon, + f1: true, + menu: { + id: MenuId.ViewContainerTitle, + when: ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), + group: 'navigation', + order: 2 + }, + run: async (accessor: ServicesAccessor) => { + const viewPaneContainer = accessor.get(IViewsService).getActiveViewPaneContainerWithId(VIEWLET_ID); + if (viewPaneContainer) { + await (viewPaneContainer as IExtensionsViewPaneContainer).refresh(); + } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowBuiltInExtensionsAction.ID, - title: { value: ShowBuiltInExtensionsAction.LABEL, original: 'Show Built-in Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowBuiltInExtensionsAction, ShowBuiltInExtensionsAction.ID, ShowBuiltInExtensionsAction.LABEL)); + this.registerExtensionAction({ + id: 'workbench.extensions.action.installWorkspaceRecommendedExtensions', + title: localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), + icon: installWorkspaceRecommendedIcon, + menu: { + id: MenuId.ViewTitle, + when: ContextKeyEqualsExpr.create('view', WORKSPACE_RECOMMENDATIONS_VIEW_ID), + group: 'navigation', + order: 1 + }, + run: async (accessor: ServicesAccessor) => { + const view = accessor.get(IViewsService).getActiveViewWithId(WORKSPACE_RECOMMENDATIONS_VIEW_ID) as IWorkspaceRecommendedExtensionsView; + return view.installWorkspaceRecommendations(); } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: UpdateAllAction.ID, - title: { value: UpdateAllAction.LABEL, original: 'Update All Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(UpdateAllAction, UpdateAllAction.ID, UpdateAllAction.LABEL, false)); - } + this.registerExtensionAction({ + id: ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, + title: ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL, + icon: configureRecommendedIcon, + menu: [{ + id: MenuId.CommandPalette, + when: WorkbenchStateContext.notEqualsTo('empty'), + }, { + id: MenuId.ViewTitle, + when: ContextKeyEqualsExpr.create('view', WORKSPACE_RECOMMENDATIONS_VIEW_ID), + group: 'navigation', + order: 2 + }], + run: () => runAction(this.instantiationService.createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL)) }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: InstallVSIXAction.ID, - title: { value: InstallVSIXAction.LABEL, original: 'Install from VSIX...' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL)); - } + this.registerExtensionAction({ + id: InstallSpecificVersionOfExtensionAction.ID, + title: { value: InstallSpecificVersionOfExtensionAction.LABEL, original: 'Install Specific Version of Extension...' }, + category: ExtensionsLocalizedLabel, + menu: { + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + }, + run: () => runAction(this.instantiationService.createInstance(InstallSpecificVersionOfExtensionAction, InstallSpecificVersionOfExtensionAction.ID, InstallSpecificVersionOfExtensionAction.LABEL)) }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: DisableAllAction.ID, - title: { value: DisableAllAction.LABEL, original: 'Disable All Installed Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(DisableAllAction, DisableAllAction.ID, DisableAllAction.LABEL, false)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: DisableAllWorkspaceAction.ID, - title: { value: DisableAllWorkspaceAction.LABEL, original: 'Disable All Installed Extensions for this Workspace' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([WorkbenchStateContext.notEqualsTo('empty'), ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(DisableAllWorkspaceAction, DisableAllWorkspaceAction.ID, DisableAllWorkspaceAction.LABEL, false)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: EnableAllAction.ID, - title: { value: EnableAllAction.LABEL, original: 'Enable All Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(EnableAllAction, EnableAllAction.ID, EnableAllAction.LABEL, false)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: EnableAllWorkspaceAction.ID, - title: { value: EnableAllWorkspaceAction.LABEL, original: 'Enable All Extensions for this Workspace' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([WorkbenchStateContext.notEqualsTo('empty'), ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(EnableAllWorkspaceAction, EnableAllWorkspaceAction.ID, EnableAllWorkspaceAction.LABEL, false)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: CheckForUpdatesAction.ID, - title: { value: CheckForUpdatesAction.LABEL, original: 'Check for Extension Updates' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(CheckForUpdatesAction, CheckForUpdatesAction.ID, CheckForUpdatesAction.LABEL)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: ClearExtensionsSearchResultsAction.ID, - title: { value: ClearExtensionsSearchResultsAction.LABEL, original: 'Clear Extensions Search Results' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ClearExtensionsSearchResultsAction, ClearExtensionsSearchResultsAction.ID, ClearExtensionsSearchResultsAction.LABEL)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: RefreshExtensionsAction.ID, - title: { value: RefreshExtensionsAction.LABEL, original: 'Refresh' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(RefreshExtensionsAction, RefreshExtensionsAction.ID, RefreshExtensionsAction.LABEL)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: EnableAutoUpdateAction.ID, - title: { value: EnableAutoUpdateAction.LABEL, original: 'Enable Auto Updating Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(EnableAutoUpdateAction, EnableAutoUpdateAction.ID, EnableAutoUpdateAction.LABEL)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: DisableAutoUpdateAction.ID, - title: { value: DisableAutoUpdateAction.LABEL, original: 'Disable Auto Updating Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(DisableAutoUpdateAction, DisableAutoUpdateAction.ID, DisableAutoUpdateAction.LABEL)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: InstallSpecificVersionOfExtensionAction.ID, - title: { value: InstallSpecificVersionOfExtensionAction.LABEL, original: 'Install Specific Version of Extension...' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(InstallSpecificVersionOfExtensionAction, InstallSpecificVersionOfExtensionAction.ID, InstallSpecificVersionOfExtensionAction.LABEL)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: ReinstallAction.ID, - title: { value: ReinstallAction.LABEL, original: 'Reinstall Extension...' }, - category: CATEGORIES.Developer, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER])]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ReinstallAction, ReinstallAction.ID, ReinstallAction.LABEL)); - } + this.registerExtensionAction({ + id: ReinstallAction.ID, + title: { value: ReinstallAction.LABEL, original: 'Reinstall Extension...' }, + category: CATEGORIES.Developer, + menu: { + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER])]) + }, + run: () => runAction(this.instantiationService.createInstance(ReinstallAction, ReinstallAction.ID, ReinstallAction.LABEL)) }); } // Extension Context Menu private registerContextMenuActions(): void { - 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]; + this.registerExtensionAction({ + id: 'workbench.extensions.action.copyExtension', + title: { value: localize('workbench.extensions.action.copyExtension', "Copy"), original: 'Copy' }, + menu: { + id: MenuId.ExtensionContext, + group: '1_copy' + }, + run: async (accessor: ServicesAccessor, extensionId: string) => { + const clipboardService = accessor.get(IClipboardService); + let extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, { id: extensionId }))[0] + || (await this.extensionsWorkbenchService.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); @@ -886,165 +1040,104 @@ class ExtensionsContributions implements IWorkbenchContribution { 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); + await clipboardService.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); - } + this.registerExtensionAction({ + 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' + }, + run: async (accessor: ServicesAccessor, id: string) => accessor.get(IClipboardService).writeText(id) }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.configure', - title: { value: localize('workbench.extensions.action.configure', "Extension Settings"), original: 'Extension Settings' }, - 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}`); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.configure', + title: { value: localize('workbench.extensions.action.configure', "Extension Settings"), original: 'Extension Settings' }, + menu: { + id: MenuId.ExtensionContext, + group: '2_configure', + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.has('extensionHasConfiguration')) + }, + run: async (accessor: ServicesAccessor, id: string) => accessor.get(IPreferencesService).openSettings(false, `@ext:${id}`) }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: TOGGLE_IGNORE_EXTENSION_ACTION_ID, - title: { value: localize('workbench.extensions.action.toggleIgnoreExtension', "Sync This Extension"), original: `Sync This Extension` }, - menu: { - id: MenuId.ExtensionContext, - group: '2_configure', - when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, ContextKeyExpr.has('inExtensionEditor').negate()) - }, - }); - } - - async run(accessor: ServicesAccessor, id: string) { - const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); - const extension = extensionsWorkbenchService.local.find(e => areSameExtensions({ id }, e.identifier)); + this.registerExtensionAction({ + id: TOGGLE_IGNORE_EXTENSION_ACTION_ID, + title: { value: localize('workbench.extensions.action.toggleIgnoreExtension', "Sync This Extension"), original: `Sync This Extension` }, + menu: { + id: MenuId.ExtensionContext, + group: '2_configure', + when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, ContextKeyExpr.has('inExtensionEditor').negate()) + }, + run: async (accessor: ServicesAccessor, id: string) => { + const extension = this.extensionsWorkbenchService.local.find(e => areSameExtensions({ id }, e.identifier)); if (extension) { - return extensionsWorkbenchService.toggleExtensionIgnoredToSync(extension); + return this.extensionsWorkbenchService.toggleExtensionIgnoredToSync(extension); } } }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.ignoreRecommendation', - title: { value: localize('workbench.extensions.action.ignoreRecommendation', "Ignore Recommendation"), original: `Ignore Recommendation` }, - menu: { - id: MenuId.ExtensionContext, - group: '3_recommendations', - when: ContextKeyExpr.has('isExtensionRecommended'), - order: 1 - }, - }); - } - - async run(accessor: ServicesAccessor, id: string): Promise { - accessor.get(IExtensionIgnoredRecommendationsService).toggleGlobalIgnoredRecommendation(id, true); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.ignoreRecommendation', + title: { value: localize('workbench.extensions.action.ignoreRecommendation', "Ignore Recommendation"), original: `Ignore Recommendation` }, + menu: { + id: MenuId.ExtensionContext, + group: '3_recommendations', + when: ContextKeyExpr.has('isExtensionRecommended'), + order: 1 + }, + run: async (accessor: ServicesAccessor, id: string) => accessor.get(IExtensionIgnoredRecommendationsService).toggleGlobalIgnoredRecommendation(id, true) }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.undoIgnoredRecommendation', - title: { value: localize('workbench.extensions.action.undoIgnoredRecommendation', "Undo Ignored Recommendation"), original: `Undo Ignored Recommendation` }, - menu: { - id: MenuId.ExtensionContext, - group: '3_recommendations', - when: ContextKeyExpr.has('isUserIgnoredRecommendation'), - order: 1 - }, - }); - } - - async run(accessor: ServicesAccessor, id: string): Promise { - accessor.get(IExtensionIgnoredRecommendationsService).toggleGlobalIgnoredRecommendation(id, false); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.undoIgnoredRecommendation', + title: { value: localize('workbench.extensions.action.undoIgnoredRecommendation', "Undo Ignored Recommendation"), original: `Undo Ignored Recommendation` }, + menu: { + id: MenuId.ExtensionContext, + group: '3_recommendations', + when: ContextKeyExpr.has('isUserIgnoredRecommendation'), + order: 1 + }, + run: async (accessor: ServicesAccessor, id: string) => accessor.get(IExtensionIgnoredRecommendationsService).toggleGlobalIgnoredRecommendation(id, false) }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.addExtensionToWorkspaceRecommendations', - title: { value: localize('workbench.extensions.action.addExtensionToWorkspaceRecommendations', "Add to Workspace Recommendations"), original: `Add to Workspace Recommendations` }, - menu: { - id: MenuId.ExtensionContext, - group: '3_recommendations', - when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('empty'), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.has('isExtensionWorkspaceRecommended').negate(), ContextKeyExpr.has('isUserIgnoredRecommendation').negate()), - order: 2 - }, - }); - } - - run(accessor: ServicesAccessor, id: string): Promise { - return accessor.get(IWorkpsaceExtensionsConfigService).toggleRecommendation(id); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.addExtensionToWorkspaceRecommendations', + title: { value: localize('workbench.extensions.action.addExtensionToWorkspaceRecommendations', "Add to Workspace Recommendations"), original: `Add to Workspace Recommendations` }, + menu: { + id: MenuId.ExtensionContext, + group: '3_recommendations', + when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('empty'), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.has('isExtensionWorkspaceRecommended').negate(), ContextKeyExpr.has('isUserIgnoredRecommendation').negate()), + order: 2 + }, + run: (accessor: ServicesAccessor, id: string) => accessor.get(IWorkpsaceExtensionsConfigService).toggleRecommendation(id) }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.removeExtensionFromWorkspaceRecommendations', - title: { value: localize('workbench.extensions.action.removeExtensionFromWorkspaceRecommendations', "Remove from Workspace Recommendations"), original: `Remove from Workspace Recommendations` }, - menu: { - id: MenuId.ExtensionContext, - group: '3_recommendations', - when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('empty'), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.has('isExtensionWorkspaceRecommended')), - order: 2 - }, - }); - } - - run(accessor: ServicesAccessor, id: string): Promise { - return accessor.get(IWorkpsaceExtensionsConfigService).toggleRecommendation(id); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.removeExtensionFromWorkspaceRecommendations', + title: { value: localize('workbench.extensions.action.removeExtensionFromWorkspaceRecommendations', "Remove from Workspace Recommendations"), original: `Remove from Workspace Recommendations` }, + menu: { + id: MenuId.ExtensionContext, + group: '3_recommendations', + when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('empty'), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.has('isExtensionWorkspaceRecommended')), + order: 2 + }, + run: (accessor: ServicesAccessor, id: string) => accessor.get(IWorkpsaceExtensionsConfigService).toggleRecommendation(id) }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.addToWorkspaceRecommendations', - title: { value: localize('workbench.extensions.action.addToWorkspaceRecommendations', "Add Extension to Workspace Recommendations"), original: `Add Extension to Workspace Recommendations` }, - category: localize('extensions', "Extensions"), - menu: { - id: MenuId.CommandPalette, - when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), - }, - }); - } - + this.registerExtensionAction({ + id: 'workbench.extensions.action.addToWorkspaceRecommendations', + title: { value: localize('workbench.extensions.action.addToWorkspaceRecommendations', "Add Extension to Workspace Recommendations"), original: `Add Extension to Workspace Recommendations` }, + category: localize('extensions', "Extensions"), + menu: { + id: MenuId.CommandPalette, + when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), + }, async run(accessor: ServicesAccessor): Promise { const editorService = accessor.get(IEditorService); const workpsaceExtensionsConfigService = accessor.get(IWorkpsaceExtensionsConfigService); @@ -1060,39 +1153,25 @@ class ExtensionsContributions implements IWorkbenchContribution { } }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.addToWorkspaceFolderRecommendations', - title: { value: localize('workbench.extensions.action.addToWorkspaceFolderRecommendations', "Add Extension to Workspace Folder Recommendations"), original: `Add Extension to Workspace Folder Recommendations` }, - category: localize('extensions', "Extensions"), - menu: { - id: MenuId.CommandPalette, - when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('folder'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), - }, - }); - } - - async run(accessor: ServicesAccessor): Promise { - return accessor.get(ICommandService).executeCommand('workbench.extensions.action.addToWorkspaceRecommendations'); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.addToWorkspaceFolderRecommendations', + title: { value: localize('workbench.extensions.action.addToWorkspaceFolderRecommendations', "Add Extension to Workspace Folder Recommendations"), original: `Add Extension to Workspace Folder Recommendations` }, + category: localize('extensions', "Extensions"), + menu: { + id: MenuId.CommandPalette, + when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('folder'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), + }, + run: () => this.commandService.executeCommand('workbench.extensions.action.addToWorkspaceRecommendations') }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.addToWorkspaceIgnoredRecommendations', - title: { value: localize('workbench.extensions.action.addToWorkspaceIgnoredRecommendations', "Add Extension to Workspace Ignored Recommendations"), original: `Add Extension to Workspace Ignored Recommendations` }, - category: localize('extensions', "Extensions"), - menu: { - id: MenuId.CommandPalette, - when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), - }, - }); - } - + this.registerExtensionAction({ + id: 'workbench.extensions.action.addToWorkspaceIgnoredRecommendations', + title: { value: localize('workbench.extensions.action.addToWorkspaceIgnoredRecommendations', "Add Extension to Workspace Ignored Recommendations"), original: `Add Extension to Workspace Ignored Recommendations` }, + category: localize('extensions', "Extensions"), + menu: { + id: MenuId.CommandPalette, + when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), + }, async run(accessor: ServicesAccessor): Promise { const editorService = accessor.get(IEditorService); const workpsaceExtensionsConfigService = accessor.get(IWorkpsaceExtensionsConfigService); @@ -1108,63 +1187,65 @@ class ExtensionsContributions implements IWorkbenchContribution { } }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations', - title: { value: localize('workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations', "Add Extension to Workspace Folder Ignored Recommendations"), original: `Add Extension to Workspace Folder Ignored Recommendations` }, - category: localize('extensions', "Extensions"), - menu: { - id: MenuId.CommandPalette, - when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('folder'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), - }, - }); - } - - run(accessor: ServicesAccessor): Promise { - return accessor.get(ICommandService).executeCommand('workbench.extensions.action.addToWorkspaceIgnoredRecommendations'); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations', + title: { value: localize('workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations', "Add Extension to Workspace Folder Ignored Recommendations"), original: `Add Extension to Workspace Folder Ignored Recommendations` }, + category: localize('extensions', "Extensions"), + menu: { + id: MenuId.CommandPalette, + when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('folder'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), + }, + run: () => this.commandService.executeCommand('workbench.extensions.action.addToWorkspaceIgnoredRecommendations') }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: ConfigureWorkspaceRecommendedExtensionsAction.ID, - title: { value: ConfigureWorkspaceRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace)' }, - category: localize('extensions', "Extensions"), - menu: { - id: MenuId.CommandPalette, - when: WorkbenchStateContext.isEqualTo('workspace'), - }, - }); - } - - run(accessor: ServicesAccessor): Promise { - return accessor.get(IInstantiationService).createInstance(ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceRecommendedExtensionsAction.ID, ConfigureWorkspaceRecommendedExtensionsAction.LABEL).run(); - } + this.registerExtensionAction({ + id: ConfigureWorkspaceRecommendedExtensionsAction.ID, + title: { value: ConfigureWorkspaceRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace)' }, + category: localize('extensions', "Extensions"), + menu: { + id: MenuId.CommandPalette, + when: WorkbenchStateContext.isEqualTo('workspace'), + }, + run: () => runAction(this.instantiationService.createInstance(ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceRecommendedExtensionsAction.ID, ConfigureWorkspaceRecommendedExtensionsAction.LABEL)) }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, - title: { value: ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace Folder)' }, - category: localize('extensions', "Extensions"), - menu: { - id: MenuId.CommandPalette, - when: WorkbenchStateContext.notEqualsTo('empty'), - }, - }); - } - - run(accessor: ServicesAccessor): Promise { - return accessor.get(IInstantiationService).createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL).run(); - } - }); } + + private registerExtensionAction(extensionActionOptions: IExtensionActionOptions): IDisposable { + const menus = extensionActionOptions.menu ? isArray(extensionActionOptions.menu) ? extensionActionOptions.menu : [extensionActionOptions.menu] : []; + let menusWithOutTitles: ({ id: MenuId } & Omit)[] = []; + const menusWithTitles: { id: MenuId, item: IMenuItem }[] = []; + if (extensionActionOptions.menuTitles) { + for (let index = 0; index < menus.length; index++) { + const menu = menus[index]; + const menuTitle = extensionActionOptions.menuTitles[menu.id.id]; + if (menuTitle) { + menusWithTitles.push({ id: menu.id, item: { ...menu, command: { id: extensionActionOptions.id, title: menuTitle } } }); + } else { + menusWithOutTitles.push(menu); + } + } + } else { + menusWithOutTitles = menus; + } + const disposables = new DisposableStore(); + disposables.add(registerAction2(class extends Action2 { + constructor() { + super({ + ...extensionActionOptions, + menu: menusWithOutTitles + }); + } + run(accessor: ServicesAccessor, ...args: any[]): Promise { + return extensionActionOptions.run(accessor, ...args); + } + })); + if (menusWithTitles.length) { + disposables.add(MenuRegistry.appendMenuItems(menusWithTitles)); + } + return disposables; + } + } const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); @@ -1175,7 +1256,6 @@ workbenchRegistry.registerWorkbenchContribution(KeymapExtensions, LifecyclePhase workbenchRegistry.registerWorkbenchContribution(ExtensionsViewletViewsContribution, LifecyclePhase.Starting); workbenchRegistry.registerWorkbenchContribution(ExtensionActivationProgress, LifecyclePhase.Eventually); workbenchRegistry.registerWorkbenchContribution(ExtensionDependencyChecker, LifecyclePhase.Eventually); -workbenchRegistry.registerWorkbenchContribution(RemoteExtensionsInstaller, LifecyclePhase.Eventually); // Running Extensions const actionRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 36dbf7c7..f12e706e 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -12,7 +12,7 @@ import { Event } from 'vs/base/common/event'; import * as json from 'vs/base/common/json'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { dispose } from 'vs/base/common/lifecycle'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, AutoUpdateConfigurationKey, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; import { IGalleryExtension, IExtensionGalleryService, INSTALL_ERROR_MALICIOUS, INSTALL_ERROR_INCOMPATIBLE, IGalleryExtensionVersion, ILocalExtension, INSTALL_ERROR_NOT_SUPPORTED, InstallOptions, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensioManagementService, IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -20,9 +20,7 @@ import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService, import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, ExtensionIdentifier, IExtensionDescription, IExtensionManifest, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ShowViewletAction } from 'vs/workbench/browser/viewlet'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery'; import { IFileService, IFileContent } from 'vs/platform/files/common/files'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IHostService } from 'vs/workbench/services/host/browser/host'; @@ -40,12 +38,9 @@ import { MenuId, IMenuService } from 'vs/platform/actions/common/actions'; import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IQuickPickItem, IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { coalesce } from 'vs/base/common/arrays'; import { IWorkbenchThemeService, IWorkbenchTheme, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IWorkbenchProductIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; @@ -53,9 +48,8 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { prefersExecuteOnUI, prefersExecuteOnWorkspace, canExecuteOnUI, canExecuteOnWorkspace, prefersExecuteOnWeb } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; -import { IViewsService } from 'vs/workbench/common/views'; import { IActionViewItemOptions, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { EXTENSIONS_CONFIG, IExtensionsConfigContent } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig'; import { getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors'; @@ -64,7 +58,7 @@ import { ActionWithDropdownActionViewItem, IActionWithDropdownActionViewItemOpti import { IContextMenuProvider } from 'vs/base/browser/contextmenu'; import { ILogService } from 'vs/platform/log/common/log'; import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; -import { clearSearchResultsIcon, infoIcon, manageExtensionIcon, refreshIcon, syncEnabledIcon, syncIgnoredIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; +import { infoIcon, manageExtensionIcon, syncEnabledIcon, syncIgnoredIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; function getRelativeDateLabel(date: Date): string { const delta = new Date().getTime() - date.getTime(); @@ -100,17 +94,18 @@ function getRelativeDateLabel(date: Date): string { return ''; } -class PromptExtensionInstallFailureAction extends Action { +export class PromptExtensionInstallFailureAction extends Action { constructor( private readonly extension: IExtension, + private readonly version: string, private readonly installOperation: InstallOperation, private readonly error: Error, @IProductService private readonly productService: IProductService, @IOpenerService private readonly openerService: IOpenerService, @INotificationService private readonly notificationService: INotificationService, @IDialogService private readonly dialogService: IDialogService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @ICommandService private readonly commandService: ICommandService, @ILogService private readonly logService: ILogService, ) { super('extension.promptExtensionInstallFailure'); @@ -134,17 +129,13 @@ class PromptExtensionInstallFailureAction extends Action { if (this.extension.gallery && this.productService.extensionsGallery) { promptChoices.push({ label: localize('download', "Try Downloading Manually..."), - run: () => this.openerService.open(URI.parse(`${this.productService.extensionsGallery!.serviceUrl}/publishers/${this.extension.publisher}/vsextensions/${this.extension.name}/${this.extension.version}/vspackage`)).then(() => { + run: () => this.openerService.open(URI.parse(`${this.productService.extensionsGallery!.serviceUrl}/publishers/${this.extension.publisher}/vsextensions/${this.extension.name}/${this.version}/vspackage`)).then(() => { this.notificationService.prompt( Severity.Info, localize('install vsix', 'Once downloaded, please manually install the downloaded VSIX of \'{0}\'.', this.extension.identifier.id), [{ - label: InstallVSIXAction.LABEL, - run: () => { - const action = this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL); - action.run(); - action.dispose(); - } + label: localize('installVSIX', "Install from VSIX..."), + run: () => this.commandService.executeCommand(SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID) }] ); }) @@ -287,7 +278,7 @@ export abstract class AbstractInstallAction extends ExtensionAction { try { return await this.extensionsWorkbenchService.install(extension, this.getInstallOptions()); } catch (error) { - await this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, InstallOperation.Install, error).run(); + await this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Install, error).run(); return undefined; } } @@ -487,7 +478,7 @@ export abstract class InstallInOtherServerAction extends ExtensionAction { || !this.extension.local || this.extension.state !== ExtensionState.Installed || this.extension.type !== ExtensionType.User - || this.extension.enablementState === EnablementState.DisabledByEnvironemt + || this.extension.enablementState === EnablementState.DisabledByEnvironment ) { return false; } @@ -715,7 +706,7 @@ export class UpdateAction extends ExtensionAction { await this.extensionsWorkbenchService.install(extension); alert(localize('updateExtensionComplete', "Updating extension {0} to version {1} completed.", extension.displayName, extension.latestVersion)); } catch (err) { - this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, InstallOperation.Update, err).run(); + this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Update, err).run(); } } @@ -775,7 +766,8 @@ export class ExtensionActionWithDropdownActionViewItem extends ActionWithDropdow updateClass(): void { super.updateClass(); - if (this.dropdownMenuActionViewItem && this.dropdownMenuActionViewItem.element) { + if (this.element && this.dropdownMenuActionViewItem && this.dropdownMenuActionViewItem.element) { + this.element.classList.toggle('empty', (this._action).menuActions.length === 0); this.dropdownMenuActionViewItem.element.classList.toggle('hide', (this._action).menuActions.length === 0); } } @@ -1063,7 +1055,7 @@ export class InstallAnotherVersionAction extends ExtensionAction { await this.extensionsWorkbenchService.installVersion(this.extension!, pick.id); } } catch (error) { - this.instantiationService.createInstance(PromptExtensionInstallFailureAction, this.extension!, InstallOperation.Install, error).run(); + this.instantiationService.createInstance(PromptExtensionInstallFailureAction, this.extension!, pick.latest ? this.extension!.latestVersion : pick.id, InstallOperation.Install, error).run(); } } return null; @@ -1246,140 +1238,6 @@ export class DisableDropDownAction extends ActionWithDropDownAction { } -export class CheckForUpdatesAction extends Action { - - static readonly ID = 'workbench.extensions.action.checkForUpdates'; - static readonly LABEL = localize('checkForUpdates', "Check for Extension Updates"); - - constructor( - id = CheckForUpdatesAction.ID, - label = CheckForUpdatesAction.LABEL, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, - @IViewletService private readonly viewletService: IViewletService, - @INotificationService private readonly notificationService: INotificationService - ) { - super(id, label, '', true); - } - - private checkUpdatesAndNotify(): void { - const outdated = this.extensionsWorkbenchService.outdated; - if (!outdated.length) { - this.notificationService.info(localize('noUpdatesAvailable', "All extensions are up to date.")); - return; - } - - let msgAvailableExtensions = outdated.length === 1 ? localize('singleUpdateAvailable', "An extension update is available.") : localize('updatesAvailable', "{0} extension updates are available.", outdated.length); - - const disabledExtensionsCount = outdated.filter(ext => ext.local && !this.extensionEnablementService.isEnabled(ext.local)).length; - if (disabledExtensionsCount) { - if (outdated.length === 1) { - msgAvailableExtensions = localize('singleDisabledUpdateAvailable', "An update to an extension which is disabled is available."); - } else if (disabledExtensionsCount === 1) { - msgAvailableExtensions = localize('updatesAvailableOneDisabled', "{0} extension updates are available. One of them is for a disabled extension.", outdated.length); - } else if (disabledExtensionsCount === outdated.length) { - msgAvailableExtensions = localize('updatesAvailableAllDisabled', "{0} extension updates are available. All of them are for disabled extensions.", outdated.length); - } else { - msgAvailableExtensions = localize('updatesAvailableIncludingDisabled', "{0} extension updates are available. {1} of them are for disabled extensions.", outdated.length, disabledExtensionsCount); - } - } - - this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => viewlet.search('')); - - this.notificationService.info(msgAvailableExtensions); - } - - run(): Promise { - return this.extensionsWorkbenchService.checkForUpdates().then(() => this.checkUpdatesAndNotify()); - } -} - -export class ToggleAutoUpdateAction extends Action { - - constructor( - id: string, - label: string, - private autoUpdateValue: boolean, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(id, label, '', true); - this.updateEnablement(); - configurationService.onDidChangeConfiguration(() => this.updateEnablement()); - } - - private updateEnablement(): void { - this.enabled = this.configurationService.getValue(AutoUpdateConfigurationKey) !== this.autoUpdateValue; - } - - run(): Promise { - return this.configurationService.updateValue(AutoUpdateConfigurationKey, this.autoUpdateValue); - } -} - -export class EnableAutoUpdateAction extends ToggleAutoUpdateAction { - - static readonly ID = 'workbench.extensions.action.enableAutoUpdate'; - static readonly LABEL = localize('enableAutoUpdate', "Enable Auto Updating Extensions"); - - constructor( - id = EnableAutoUpdateAction.ID, - label = EnableAutoUpdateAction.LABEL, - @IConfigurationService configurationService: IConfigurationService - ) { - super(id, label, true, configurationService); - } -} - -export class DisableAutoUpdateAction extends ToggleAutoUpdateAction { - - static readonly ID = 'workbench.extensions.action.disableAutoUpdate'; - static readonly LABEL = localize('disableAutoUpdate', "Disable Auto Updating Extensions"); - - constructor( - id = EnableAutoUpdateAction.ID, - label = EnableAutoUpdateAction.LABEL, - @IConfigurationService configurationService: IConfigurationService - ) { - super(id, label, false, configurationService); - } -} - -export class UpdateAllAction extends Action { - - static readonly ID = 'workbench.extensions.action.updateAllExtensions'; - static readonly LABEL = localize('updateAll', "Update All Extensions"); - - constructor( - id: string, label: string, isPrimary: boolean, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - ) { - super(id, label, '', false); - - if (isPrimary) { - this._register(this.extensionsWorkbenchService.onChange(() => this._onDidChange.fire({ enabled: this.enabled }))); - } - } - - get enabled(): boolean { - return this.extensionsWorkbenchService.outdated.length > 0; - } - - run(): Promise { - return Promise.all(this.extensionsWorkbenchService.outdated.map(e => this.install(e))); - } - - private async install(extension: IExtension): Promise { - try { - await this.extensionsWorkbenchService.install(extension); - } catch (err) { - this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, InstallOperation.Update, err).run(); - } - } -} - export class ReloadAction extends ExtensionAction { private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} reload`; @@ -1722,296 +1580,6 @@ export class SetProductIconThemeAction extends ExtensionAction { } } -export class OpenExtensionsViewletAction extends ShowViewletAction { - - static ID = VIEWLET_ID; - static LABEL = localize('toggleExtensionsViewlet', "Show Extensions"); - - constructor( - id: string, - label: string, - @IViewletService viewletService: IViewletService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService - ) { - super(id, label, VIEWLET_ID, viewletService, editorGroupService, layoutService); - } -} - -export class InstallExtensionsAction extends OpenExtensionsViewletAction { - static ID = 'workbench.extensions.action.installExtensions'; - static LABEL = localize('installExtensions', "Install Extensions"); -} - -export class ShowEnabledExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showEnabledExtensions'; - static readonly LABEL = localize('showEnabledExtensions', "Show Enabled Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@enabled '); - viewlet.focus(); - }); - } -} - -export class ShowInstalledExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showInstalledExtensions'; - static readonly LABEL = localize('showInstalledExtensions', "Show Installed Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@installed '); - viewlet.focus(); - }); - } -} - -export class ShowDisabledExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showDisabledExtensions'; - static readonly LABEL = localize('showDisabledExtensions', "Show Disabled Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, 'null', true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@disabled '); - viewlet.focus(); - }); - } -} - -export class ClearExtensionsSearchResultsAction extends Action { - - static readonly ID = 'workbench.extensions.action.clearExtensionsSearchResults'; - static readonly LABEL = localize('clearExtensionsSearchResults', "Clear Extensions Search Results"); - - constructor( - id: string, - label: string, - @IViewsService private readonly viewsService: IViewsService - ) { - super(id, label, ThemeIcon.asClassName(clearSearchResultsIcon), true); - } - - async run(): Promise { - const viewPaneContainer = this.viewsService.getActiveViewPaneContainerWithId(VIEWLET_ID); - if (viewPaneContainer) { - const extensionsViewPaneContainer = viewPaneContainer as IExtensionsViewPaneContainer; - extensionsViewPaneContainer.search(''); - extensionsViewPaneContainer.focus(); - } - } -} - -export class ClearExtensionsInputAction extends ClearExtensionsSearchResultsAction { - - constructor( - id: string, - label: string, - onSearchChange: Event, - getValue: () => string, - @IViewsService viewsService: IViewsService - ) { - super(id, label, viewsService); - this.onSearchChange(getValue()); - this._register(onSearchChange(this.onSearchChange, this)); - } - - private onSearchChange(value: string): void { - this.enabled = !!value; - } -} - -export class RefreshExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.refreshExtension'; - static readonly LABEL = localize('refreshExtension', "Refresh"); - - constructor( - id: string, - label: string, - @IViewsService private readonly viewsService: IViewsService - ) { - super(id, label, ThemeIcon.asClassName(refreshIcon), true); - } - - async run(): Promise { - const viewPaneContainer = this.viewsService.getActiveViewPaneContainerWithId(VIEWLET_ID); - if (viewPaneContainer) { - const extensionsViewPaneContainer = viewPaneContainer as IExtensionsViewPaneContainer; - return extensionsViewPaneContainer.refresh(); - } - } -} - -export class ShowBuiltInExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.listBuiltInExtensions'; - static readonly LABEL = localize('showBuiltInExtensions', "Show Built-in Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@builtin '); - viewlet.focus(); - }); - } -} - -export class ShowOutdatedExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.listOutdatedExtensions'; - static readonly LABEL = localize('showOutdatedExtensions', "Show Outdated Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@outdated '); - viewlet.focus(); - }); - } -} - -export class ShowPopularExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showPopularExtensions'; - static readonly LABEL = localize('showPopularExtensions', "Show Popular Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@popular '); - viewlet.focus(); - }); - } -} - -export class PredefinedExtensionFilterAction extends Action { - - constructor( - id: string, - label: string, - private readonly filter: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search(`${this.filter} `); - viewlet.focus(); - }); - } -} - -export class RecentlyPublishedExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.recentlyPublishedExtensions'; - static readonly LABEL = localize('recentlyPublishedExtensions', "Recently Published Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@sort:publishedDate '); - viewlet.focus(); - }); - } -} - -export class ShowRecommendedExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showRecommendedExtensions'; - static readonly LABEL = localize('showRecommendedExtensions', "Show Recommended Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@recommended '); - viewlet.focus(); - }); - } -} - export class ShowRecommendedExtensionAction extends Action { static readonly ID = 'workbench.extensions.action.showRecommendedExtension'; @@ -2075,7 +1643,7 @@ export class InstallRecommendedExtensionAction extends Action { try { await this.extensionWorkbenchService.install(extension); } catch (err) { - this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, InstallOperation.Install, err).run(); + this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Install, err).run(); } } } @@ -2127,68 +1695,6 @@ export class UndoIgnoreExtensionRecommendationAction extends Action { } } -export class ShowRecommendedKeymapExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showRecommendedKeymapExtensions'; - static readonly LABEL = localize('showRecommendedKeymapExtensionsShort', "Keymaps"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@recommended:keymaps '); - viewlet.focus(); - }); - } -} - -export class ShowLanguageExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showLanguageExtensions'; - static readonly LABEL = localize('showLanguageExtensionsShort', "Language Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@category:"programming languages" @sort:installs '); - viewlet.focus(); - }); - } -} - -export class SearchCategoryAction extends Action { - - constructor( - id: string, - label: string, - private readonly category: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return new SearchExtensionsAction(`@category:"${this.category.toLowerCase()}"`, this.viewletService).run(); - } -} - export class SearchExtensionsAction extends Action { constructor( @@ -2205,46 +1711,6 @@ export class SearchExtensionsAction extends Action { } } -export class ChangeSortAction extends Action { - - private query: Query; - - constructor( - id: string, - label: string, - onSearchChange: Event, - private sortBy: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - - if (sortBy === undefined) { - throw new Error('bad arguments'); - } - - this.query = Query.parse(''); - this.enabled = false; - this.checked = false; - this._register(onSearchChange(this.onSearchChange, this)); - } - - private onSearchChange(value: string): void { - const query = Query.parse(value); - this.query = new Query(query.value, this.sortBy || query.sortBy, query.groupBy); - this.enabled = !!value && this.query.isValid(); - this.checked = this.enabled && this.query.equals(query); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search(this.query.toString()); - viewlet.focus(); - }); - } -} - export abstract class AbstractConfigureRecommendedExtensionsAction extends Action { constructor( @@ -2383,12 +1849,6 @@ export class ConfigureWorkspaceFolderRecommendedExtensionsAction extends Abstrac @ICommandService private readonly commandService: ICommandService ) { super(id, label, contextService, fileService, textFileService, editorService, jsonEditingService, textModelResolverService); - this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.update(), this)); - this.update(); - } - - private update(): void { - this.enabled = this.contextService.getWorkspace().folders.length > 0; } public run(): Promise { @@ -2735,156 +2195,6 @@ export class SystemDisabledWarningAction extends ExtensionAction { } } -export class DisableAllAction extends Action { - - static readonly ID = 'workbench.extensions.action.disableAll'; - static readonly LABEL = localize('disableAll', "Disable All Installed Extensions"); - - constructor( - id: string, label: string, isPrimary: boolean, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService - ) { - super(id, label); - if (isPrimary) { - this._register(this.extensionsWorkbenchService.onChange(() => this._onDidChange.fire({ enabled: this.enabled }))); - } - } - - private getExtensionsToDisable(): IExtension[] { - return this.extensionsWorkbenchService.local.filter(e => !e.isBuiltin && !!e.local && this.extensionEnablementService.isEnabled(e.local) && this.extensionEnablementService.canChangeEnablement(e.local)); - } - - get enabled(): boolean { - return this.getExtensionsToDisable().length > 0; - } - - run(): Promise { - return this.extensionsWorkbenchService.setEnablement(this.getExtensionsToDisable(), EnablementState.DisabledGlobally); - } -} - -export class DisableAllWorkspaceAction extends Action { - - static readonly ID = 'workbench.extensions.action.disableAllWorkspace'; - static readonly LABEL = localize('disableAllWorkspace', "Disable All Installed Extensions for this Workspace"); - - constructor( - id: string, label: string, isPrimary: boolean, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService - ) { - super(id, label); - if (isPrimary) { - this._register(Event.any(this.workspaceContextService.onDidChangeWorkbenchState, this.extensionsWorkbenchService.onChange)(() => this._onDidChange.fire({ enabled: this.enabled }))); - } - } - - private getExtensionsToDisable(): IExtension[] { - return this.extensionsWorkbenchService.local.filter(e => !e.isBuiltin && !!e.local && this.extensionEnablementService.isEnabled(e.local) && this.extensionEnablementService.canChangeEnablement(e.local)); - } - - get enabled(): boolean { - return this.getExtensionsToDisable().length > 0; - } - - run(): Promise { - return this.extensionsWorkbenchService.setEnablement(this.getExtensionsToDisable(), EnablementState.DisabledWorkspace); - } -} - -export class EnableAllAction extends Action { - - static readonly ID = 'workbench.extensions.action.enableAll'; - static readonly LABEL = localize('enableAll', "Enable All Extensions"); - - constructor( - id: string, label: string, isPrimary: boolean, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService - ) { - super(id, label); - if (isPrimary) { - this._register(this.extensionsWorkbenchService.onChange(() => this._onDidChange.fire({ enabled: this.enabled }))); - } - } - - private getExtensionsToEnable(): IExtension[] { - return this.extensionsWorkbenchService.local.filter(e => !!e.local && this.extensionEnablementService.canChangeEnablement(e.local) && !this.extensionEnablementService.isEnabled(e.local)); - } - - get enabled(): boolean { - return this.getExtensionsToEnable().length > 0; - } - - run(): Promise { - return this.extensionsWorkbenchService.setEnablement(this.getExtensionsToEnable(), EnablementState.EnabledGlobally); - } -} - -export class EnableAllWorkspaceAction extends Action { - - static readonly ID = 'workbench.extensions.action.enableAllWorkspace'; - static readonly LABEL = localize('enableAllWorkspace', "Enable All Extensions for this Workspace"); - - constructor( - id: string, label: string, isPrimary: boolean, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService - ) { - super(id, label); - if (isPrimary) { - this._register(Event.any(this.workspaceContextService.onDidChangeWorkbenchState, this.extensionsWorkbenchService.onChange)(() => this._onDidChange.fire({ enabled: this.enabled }))); - } - } - - private getExtensionsToEnable(): IExtension[] { - return this.extensionsWorkbenchService.local.filter(e => !!e.local && this.extensionEnablementService.canChangeEnablement(e.local) && !this.extensionEnablementService.isEnabled(e.local)); - } - - get enabled(): boolean { - return this.getExtensionsToEnable().length > 0; - } - - run(): Promise { - return this.extensionsWorkbenchService.setEnablement(this.getExtensionsToEnable(), EnablementState.EnabledWorkspace); - } -} - -export class InstallVSIXAction extends Action { - - static readonly ID = 'workbench.extensions.action.installVSIX'; - static readonly LABEL = localize('installVSIX', "Install from VSIX..."); - - constructor( - id = InstallVSIXAction.ID, - label = InstallVSIXAction.LABEL, - @IFileDialogService private readonly fileDialogService: IFileDialogService, - @ICommandService private readonly commandService: ICommandService - ) { - super(id, label, 'extension-action install-vsix', true); - } - - async run(): Promise { - const vsixPaths = await this.fileDialogService.showOpenDialog({ - title: localize('installFromVSIX', "Install from VSIX"), - filters: [{ name: 'VSIX Extensions', extensions: ['vsix'] }], - canSelectFiles: true, - canSelectMany: true, - openLabel: mnemonicButtonLabel(localize({ key: 'installButton', comment: ['&& denotes a mnemonic'] }, "&&Install")) - }); - - if (!vsixPaths) { - return; - } - - // Install extension(s), display notification(s), display @installed extensions - await this.commandService.executeCommand(INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, vsixPaths); - } -} - export class ReinstallAction extends Action { static readonly ID = 'workbench.extensions.action.reinstall'; @@ -2929,7 +2239,7 @@ export class ReinstallAction extends Action { } private reinstallExtension(extension: IExtension): Promise { - return this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, ShowInstalledExtensionsAction.LABEL).run() + return this.instantiationService.createInstance(SearchExtensionsAction, '@installed ').run() .then(() => { return this.extensionsWorkbenchService.reinstall(extension) .then(extension => { @@ -3015,7 +2325,7 @@ export class InstallSpecificVersionOfExtensionAction extends Action { } private install(extension: IExtension, version: string): Promise { - return this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, ShowInstalledExtensionsAction.LABEL).run() + return this.instantiationService.createInstance(SearchExtensionsAction, '@installed ').run() .then(() => { return this.extensionsWorkbenchService.installVersion(extension, version) .then(extension => { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index afe9ebc8..9858def3 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -9,22 +9,17 @@ import { timeout, Delayer } from 'vs/base/common/async'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { Event, Emitter } from 'vs/base/common/event'; -import { IAction, Action, Separator, SubmenuAction } from 'vs/base/common/actions'; +import { Event } from 'vs/base/common/event'; +import { Action } from 'vs/base/common/actions'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { append, $, Dimension, hide, show } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, AutoUpdateConfigurationKey, CloseExtensionDetailsOnViewChangeKey, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID } from '../common/extensions'; -import { - ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction, CheckForUpdatesAction, DisableAllAction, EnableAllAction, - EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction, InstallVSIXAction, SearchCategoryAction, - RecentlyPublishedExtensionsAction, ShowInstalledExtensionsAction, ShowOutdatedExtensionsAction, ShowDisabledExtensionsAction, - ShowEnabledExtensionsAction, PredefinedExtensionFilterAction, RefreshExtensionsAction -} from 'vs/workbench/contrib/extensions/browser/extensionsActions'; -import { IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, CloseExtensionDetailsOnViewChangeKey, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, DefaultViewsContext, ExtensionsSortByContext, WORKSPACE_RECOMMENDATIONS_VIEW_ID } from '../common/extensions'; +import { InstallLocalExtensionsInRemoteAction, InstallRemoteExtensionsInLocalAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInFeatureExtensionsView, BuiltInThemesExtensionsView, BuiltInProgrammingLanguageExtensionsView, ServerInstalledExtensionsView, DefaultRecommendedExtensionsView } from 'vs/workbench/contrib/extensions/browser/extensionsViews'; @@ -32,24 +27,25 @@ import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import Severity from 'vs/base/common/severity'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IViewsRegistry, IViewDescriptor, Extensions, ViewContainer, IViewDescriptorService, IAddedViewDescriptorRef } from 'vs/workbench/common/views'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { getMaliciousExtensionsSet } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { ViewPane, ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery'; import { SuggestEnabledInput, attachSuggestEnabledInputBoxStyler } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { createErrorWithActions } from 'vs/base/common/errorsWithActions'; -import { ExtensionType, EXTENSION_CATEGORIES } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; import { ILabelService } from 'vs/platform/label/common/label'; import { MementoObject } from 'vs/workbench/common/memento'; @@ -62,10 +58,9 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { isWeb } from 'vs/base/common/platform'; -import { memoize } from 'vs/base/common/decorators'; -import { filterIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; +import { installLocalInRemoteIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; +import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; -const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); const SearchMarketplaceExtensionsContext = new RawContextKey('searchMarketplaceExtensions', false); const SearchIntalledExtensionsContext = new RawContextKey('searchInstalledExtensions', false); const SearchOutdatedExtensionsContext = new RawContextKey('searchOutdatedExtensions', false); @@ -155,7 +150,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio get name() { return getInstalledViewName(); }, weight: 100, order: 1, - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.not('hasInstalledExtensions')), + when: ContextKeyExpr.and(DefaultViewsContext, ContextKeyExpr.not('hasInstalledExtensions')), /* Empty installed extensions view shall have fixed height */ ctorDescriptor: new SyncDescriptor(ServerInstalledExtensionsView, [{ server, fixedHeight: true, onDidChangeTitle }]), /* Empty installed extensions views shall not be allowed to hidden */ @@ -168,11 +163,49 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio get name() { return getInstalledViewName(); }, weight: 100, order: 1, - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), isWebServer ? ContextKeyExpr.has('hasInstalledWebExtensions') : ContextKeyExpr.has('hasInstalledExtensions')), + when: ContextKeyExpr.and(DefaultViewsContext, isWebServer ? ContextKeyExpr.has('hasInstalledWebExtensions') : ContextKeyExpr.has('hasInstalledExtensions')), ctorDescriptor: new SyncDescriptor(ServerInstalledExtensionsView, [{ server, onDidChangeTitle }]), /* Installed extensions views shall not be allowed to hidden when there are more than one server */ canToggleVisibility: servers.length === 1 }); + + if (server === this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManagementServerService.localExtensionManagementServer) { + registerAction2(class InstallLocalExtensionsInRemoteAction2 extends Action2 { + constructor() { + super({ + id: 'workbench.extensions.installLocalExtensions', + get title() { return localize('select and install local extensions', "Install Local Extensions in '{0}'...", server.label); }, + category: localize({ key: 'remote', comment: ['Remote as in remote machine'] }, "Remote"), + icon: installLocalInRemoteIcon, + f1: true, + menu: { + id: MenuId.ViewTitle, + when: ContextKeyEqualsExpr.create('view', id), + group: 'navigation', + } + }); + } + run(accessor: ServicesAccessor): Promise { + return accessor.get(IInstantiationService).createInstance(InstallLocalExtensionsInRemoteAction).run(); + } + }); + } + } + + if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { + registerAction2(class InstallRemoteExtensionsInLocalAction2 extends Action2 { + constructor() { + super({ + id: 'workbench.extensions.actions.installLocalExtensionsInRemote', + title: { value: localize('install remote in local', "Install Remote Extensions Locally..."), original: 'Install Remote Extensions Locally...' }, + category: localize({ key: 'remote', comment: ['Remote as in remote machine'] }, "Remote"), + f1: true + }); + } + run(accessor: ServicesAccessor): Promise { + return accessor.get(IInstantiationService).createInstance(InstallRemoteExtensionsInLocalAction, 'workbench.extensions.actions.installLocalExtensionsInRemote').run(); + } + }); } /* @@ -184,7 +217,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio id: 'workbench.views.extensions.popular', name: localize('popularExtensions', "Popular"), ctorDescriptor: new SyncDescriptor(ExtensionsListView, [{}]), - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.not('hasInstalledExtensions')), + when: ContextKeyExpr.and(DefaultViewsContext, ContextKeyExpr.not('hasInstalledExtensions')), weight: 60, order: 2, canToggleVisibility: false @@ -199,7 +232,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio id: 'extensions.recommendedList', name: localize('recommendedExtensions', "Recommended"), ctorDescriptor: new SyncDescriptor(DefaultRecommendedExtensionsView, [{}]), - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.not('config.extensions.showRecommendationsOnlyOnDemand')), + when: ContextKeyExpr.and(DefaultViewsContext, ContextKeyExpr.not('config.extensions.showRecommendationsOnlyOnDemand')), weight: 40, order: 3, canToggleVisibility: true @@ -215,7 +248,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio id: 'workbench.views.extensions.enabled', name: localize('enabledExtensions', "Enabled"), ctorDescriptor: new SyncDescriptor(EnabledExtensionsView, [{}]), - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions')), + when: ContextKeyExpr.and(DefaultViewsContext, ContextKeyExpr.has('hasInstalledExtensions')), hideByDefault: true, weight: 40, order: 4, @@ -230,7 +263,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio id: 'workbench.views.extensions.disabled', name: localize('disabledExtensions', "Disabled"), ctorDescriptor: new SyncDescriptor(DisabledExtensionsView, [{}]), - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions')), + when: ContextKeyExpr.and(DefaultViewsContext, ContextKeyExpr.has('hasInstalledExtensions')), hideByDefault: true, weight: 10, order: 5, @@ -312,7 +345,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio const viewDescriptors: IViewDescriptor[] = []; viewDescriptors.push({ - id: 'workbench.views.extensions.workspaceRecommendations', + id: WORKSPACE_RECOMMENDATIONS_VIEW_ID, name: localize('workspaceRecommendedExtensions', "Workspace Recommendations"), ctorDescriptor: new SyncDescriptor(WorkspaceRecommendedExtensionsView, [{}]), when: ContextKeyExpr.and(ContextKeyExpr.has('recommendedExtensions'), WorkbenchStateContext.notEqualsTo('empty')), @@ -361,9 +394,8 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IExtensionsViewPaneContainer { - private readonly _onSearchChange: Emitter = this._register(new Emitter()); - private readonly onSearchChange: Event = this._onSearchChange.event; private defaultViewsContextKey: IContextKey; + private sortByContextKey: IContextKey; private searchMarketplaceExtensionsContextKey: IContextKey; private searchInstalledExtensionsContextKey: IContextKey; private searchOutdatedExtensionsContextKey: IContextKey; @@ -379,8 +411,6 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE private root: HTMLElement | undefined; private searchBox: SuggestEnabledInput | undefined; private readonly searchViewletState: MementoObject; - private readonly sortActions: ChangeSortAction[]; - private secondaryActions: IAction[] | undefined = undefined; constructor( @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @@ -390,7 +420,6 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, - @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @INotificationService private readonly notificationService: INotificationService, @IViewletService private readonly viewletService: IViewletService, @IThemeService themeService: IThemeService, @@ -408,6 +437,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.searchDelayer = new Delayer(500); this.defaultViewsContextKey = DefaultViewsContext.bindTo(contextKeyService); + this.sortByContextKey = ExtensionsSortByContext.bindTo(contextKeyService); this.searchMarketplaceExtensionsContextKey = SearchMarketplaceExtensionsContext.bindTo(contextKeyService); this.searchInstalledExtensionsContextKey = SearchIntalledExtensionsContext.bindTo(contextKeyService); this.searchOutdatedExtensionsContextKey = SearchOutdatedExtensionsContext.bindTo(contextKeyService); @@ -421,13 +451,6 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this._register(this.viewletService.onDidViewletOpen(this.onViewletOpen, this)); this.searchViewletState = this.getMemento(StorageScope.WORKSPACE, StorageTarget.USER); - this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(AutoUpdateConfigurationKey)) { - this.secondaryActions = undefined; - this.updateTitleArea(); - } - }, this)); - if (extensionManagementServerService.webExtensionManagementServer) { this._register(extensionsWorkbenchService.onChange(() => { // show installed web extensions view only when it is not visible @@ -437,13 +460,10 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE } })); } + } - this.sortActions = [ - this._register(this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.install', localize('sort by installs', "Install Count"), this.onSearchChange, 'installs')), - this._register(this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.rating', localize('sort by rating', "Rating"), this.onSearchChange, 'rating')), - this._register(this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.name', localize('sort by name', "Name"), this.onSearchChange, 'name')), - this._register(this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.publishedDate', localize('sort by date', "Published Date"), this.onSearchChange, 'publishedDate')), - ]; + get searchValue(): string | undefined { + return this.searchBox?.getValue(); } create(parent: HTMLElement): void { @@ -478,8 +498,8 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this._register(attachSuggestEnabledInputBoxStyler(this.searchBox, this.themeService)); this._register(this.searchBox.onInputDidChange(() => { + this.sortByContextKey.set(Query.parse(this.searchBox!.getValue() || '').sortBy); this.triggerSearch(); - this._onSearchChange.fire(this.searchBox!.getValue()); }, this)); this._register(this.searchBox.onShouldFocusResults(() => this.focusListView(), this)); @@ -556,59 +576,6 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE return 400; } - @memoize - getActions(): IAction[] { - // Local extensions filters - let filterActions: IAction[] = [ - this._register(this.instantiationService.createInstance(ShowBuiltInExtensionsAction, ShowBuiltInExtensionsAction.ID, localize('builtin filter', "Built-in"))), - this._register(this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, localize('installed filter', "Installed"))), - this._register(this.instantiationService.createInstance(ShowEnabledExtensionsAction, ShowEnabledExtensionsAction.ID, localize('enabled filter', "Enabled"))), - this._register(this.instantiationService.createInstance(ShowDisabledExtensionsAction, ShowDisabledExtensionsAction.ID, localize('disabled filter', "Disabled"))), - this._register(this.instantiationService.createInstance(ShowOutdatedExtensionsAction, ShowOutdatedExtensionsAction.ID, localize('outdated filter', "Outdated"))), - ]; - - if (this.extensionGalleryService.isEnabled()) { - filterActions = [ - this._register(this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.featured', localize('featured filter', "Featured"), '@featured')), - this._register(this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.popular', localize('most popular filter', "Most Popular"), '@popular')), - this._register(this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.recommended', localize('most popular recommended', "Recommended"), '@recommended')), - this._register(this.instantiationService.createInstance(RecentlyPublishedExtensionsAction, RecentlyPublishedExtensionsAction.ID, localize('recently published filter', "Recently Published"))), - this._register(new Separator()), - this._register(new SubmenuAction('workbench.extensions.action.filterExtensionsByCategory', localize('filter by category', "Category"), EXTENSION_CATEGORIES.map(category => this.instantiationService.createInstance(SearchCategoryAction, `extensions.actions.searchByCategory.${category}`, category, category)))), - this._register(new Separator()), - ...filterActions, - this._register(new Separator()), - this._register(new SubmenuAction('workbench.extensions.action.sortBy', localize('sorty by', "Sort By"), this.sortActions)), - ]; - } - - return [ - this._register(new SubmenuAction('workbench.extensions.action.filterExtensions', localize('filterExtensions', "Filter Extensions..."), filterActions, ThemeIcon.asClassName(filterIcon))), - this._register(this.instantiationService.createInstance(RefreshExtensionsAction, RefreshExtensionsAction.ID, RefreshExtensionsAction.LABEL)), - this._register(this.instantiationService.createInstance(ClearExtensionsInputAction, ClearExtensionsInputAction.ID, ClearExtensionsInputAction.LABEL, this.onSearchChange, () => this.searchBox!.getValue() || '')), - ]; - } - - getSecondaryActions(): IAction[] { - if (!this.secondaryActions) { - this.secondaryActions = []; - this.secondaryActions.push(this.instantiationService.createInstance(CheckForUpdatesAction, CheckForUpdatesAction.ID, CheckForUpdatesAction.LABEL)); - if (this.configurationService.getValue(AutoUpdateConfigurationKey)) { - this.secondaryActions.push(this.instantiationService.createInstance(DisableAutoUpdateAction, DisableAutoUpdateAction.ID, DisableAutoUpdateAction.LABEL)); - } else { - this.secondaryActions.push(this.instantiationService.createInstance(UpdateAllAction, UpdateAllAction.ID, UpdateAllAction.LABEL, false), this.instantiationService.createInstance(EnableAutoUpdateAction, EnableAutoUpdateAction.ID, EnableAutoUpdateAction.LABEL)); - } - - this.secondaryActions.push(new Separator()); - this.secondaryActions.push(this.instantiationService.createInstance(EnableAllAction, EnableAllAction.ID, EnableAllAction.LABEL, false)); - this.secondaryActions.push(this.instantiationService.createInstance(DisableAllAction, DisableAllAction.ID, DisableAllAction.LABEL, false)); - - this.secondaryActions.push(new Separator()); - this.secondaryActions.push(this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL)); - } - return this.secondaryActions; - } - search(value: string): void { if (this.searchBox && this.searchBox.getValue() !== value) { this.searchBox.setValue(value); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 932a76b6..be8f1ac3 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -17,19 +17,19 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { append, $ } from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Delegate, Renderer, IExtensionsViewState, EXTENSION_LIST_ELEMENT_HEIGHT } from 'vs/workbench/contrib/extensions/browser/extensionsList'; -import { ExtensionState, IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { ExtensionState, IExtension, IExtensionsWorkbenchService, IWorkspaceRecommendedExtensionsView } from 'vs/workbench/contrib/extensions/common/extensions'; import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery'; import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; -import { ConfigureWorkspaceFolderRecommendedExtensionsAction, ManageExtensionAction, InstallLocalExtensionsInRemoteAction, getContextMenuActions, ExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { ManageExtensionAction, getContextMenuActions, ExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { WorkbenchPagedList, ListResourceNavigator } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { coalesce, distinct, flatten } from 'vs/base/common/arrays'; import { IExperimentService, IExperiment, ExperimentActionType } from 'vs/workbench/contrib/experiments/common/experimentService'; @@ -49,7 +49,6 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { configureRecommendedIcon, installLocalInRemoteIcon, installWorkspaceRecommendedIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; // Extensions that are automatically classified as Programming Language extensions, but should be Feature extensions const FORCE_FEATURE_EXTENSIONS = ['vscode.git', 'vscode.search-result']; @@ -134,8 +133,12 @@ export class ExtensionsListView extends ViewPane { if (this.options.onDidChangeTitle) { this._register(this.options.onDidChangeTitle(title => this.updateTitle(title))); } + + this.registerActions(); } + protected registerActions(): void { } + protected renderHeader(container: HTMLElement): void { container.classList.add('extension-view-header'); super.renderHeader(container); @@ -263,32 +266,27 @@ export class ExtensionsListView extends ViewPane { const runningExtensions = await this.extensionService.getExtensions(); const manageExtensionAction = this.instantiationService.createInstance(ManageExtensionAction); manageExtensionAction.extension = e.element; + let groups: IAction[][] = []; if (manageExtensionAction.enabled) { - const groups = await manageExtensionAction.getActionGroups(runningExtensions); - 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) - }); + groups = await manageExtensionAction.getActionGroups(runningExtensions); + } else if (e.element) { - const groups = getContextMenuActions(e.element, false, this.instantiationService); + groups = getContextMenuActions(e.element, false, this.instantiationService); groups.forEach(group => group.forEach(extensionAction => { if (extensionAction instanceof 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 - }); } + let actions: IAction[] = []; + for (const menuActions of groups) { + actions = [...actions, ...menuActions, new Separator()]; + } + actions.pop(); + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => actions + }); } } @@ -693,8 +691,14 @@ export class ExtensionsListView extends ViewPane { } // All recommendations - if (/@recommended:all/i.test(query.value) || ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value)) { - return this.getAllRecommendationsModel(query, options, token); + if (/@recommended:all/i.test(query.value)) { + return this.getAllRecommendationsModel(options, token); + } + + // Search recommendations + if (ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value) || + (ExtensionsListView.isRecommendedExtensionsQuery(query.value) && options.sortBy !== undefined)) { + return this.searchRecommendations(query, options, token); } // Other recommendations @@ -730,10 +734,8 @@ export class ExtensionsListView extends ViewPane { } private async getWorkspaceRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { - const value = query.value.replace(/@recommended:workspace/g, '').trim().toLowerCase(); const recommendations = await this.getWorkspaceRecommendations(); - const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-workspace' }, token)) - .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); + const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-workspace' }, token)); this.telemetryService.publicLog2<{ count: number }, WorkspaceRecommendationsClassification>('extensionWorkspaceRecommendations:open', { count: installableRecommendations.length }); const result: IExtension[] = coalesce(recommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); return new PagedModel(result); @@ -755,14 +757,19 @@ export class ExtensionsListView extends ViewPane { } private async getOtherRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { - const value = query.value.replace(/@recommended/g, '').trim().toLowerCase(); + const otherRecommendations = await this.getOtherRecommendations(); + const installableRecommendations = await this.getInstallableRecommendations(otherRecommendations, { ...options, source: 'recommendations-other', sortBy: undefined }, token); + const result = coalesce(otherRecommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); + return new PagedModel(result); + } + private async getOtherRecommendations(): Promise { const local = (await this.extensionsWorkbenchService.queryLocal(this.options.server)) .map(e => e.identifier.id.toLowerCase()); const workspaceRecommendations = (await this.getWorkspaceRecommendations()) .map(extensionId => extensionId.toLowerCase()); - const otherRecommendations = distinct( + return distinct( flatten(await Promise.all([ // Order is important this.extensionRecommendationsService.getImportantRecommendations(), @@ -770,16 +777,10 @@ export class ExtensionsListView extends ViewPane { this.extensionRecommendationsService.getOtherRecommendations() ])).filter(extensionId => !local.includes(extensionId.toLowerCase()) && !workspaceRecommendations.includes(extensionId.toLowerCase()) ), extensionId => extensionId.toLowerCase()); - - const installableRecommendations = (await this.getInstallableRecommendations(otherRecommendations, { ...options, source: 'recommendations-other', sortBy: undefined }, token)) - .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); - - const result: IExtension[] = coalesce(otherRecommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); - return new PagedModel(result); } // Get All types of recommendations, trimmed to show a max of 8 at any given time - private async getAllRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + private async getAllRecommendationsModel(options: IQueryOptions, token: CancellationToken): Promise> { const local = (await this.extensionsWorkbenchService.queryLocal(this.options.server)).map(e => e.identifier.id.toLowerCase()); const allRecommendations = distinct( @@ -797,6 +798,15 @@ export class ExtensionsListView extends ViewPane { return new PagedModel(result.slice(0, 8)); } + private async searchRecommendations(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + const value = query.value.replace(/@recommended/g, '').trim().toLowerCase(); + const recommendations = distinct([...await this.getWorkspaceRecommendations(), ...await this.getOtherRecommendations()]); + const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations', sortBy: undefined }, token)) + .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); + const result = coalesce(recommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); + return new PagedModel(this.sortExtensions(result, options)); + } + // Sorts the firstPage of the pager in the same order as given array of extension ids private sortFirstPage(pager: IPager, ids: string[]) { ids = ids.map(x => x.toLowerCase()); @@ -951,7 +961,7 @@ export class ExtensionsListView extends ViewPane { } static isSearchRecommendedExtensionsQuery(query: string): boolean { - return /@recommended/i.test(query) && !ExtensionsListView.isRecommendedExtensionsQuery(query); + return /@recommended\s.+/i.test(query); } static isWorkspaceRecommendedExtensionsQuery(query: string): boolean { @@ -989,14 +999,6 @@ export class ServerInstalledExtensionsView extends ExtensionsListView { return super.show(query.trim()); } - getActions(): IAction[] { - if (this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManagementServerService.localExtensionManagementServer === this.options.server) { - const installLocalExtensionsInRemoteAction = this._register(this.instantiationService.createInstance(InstallLocalExtensionsInRemoteAction)); - installLocalExtensionsInRemoteAction.class = ThemeIcon.asClassName(installLocalInRemoteIcon); - return [installLocalExtensionsInRemoteAction]; - } - return []; - } } export class EnabledExtensionsView extends ExtensionsListView { @@ -1074,9 +1076,8 @@ export class RecommendedExtensionsView extends ExtensionsListView { } } -export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { +export class WorkspaceRecommendedExtensionsView extends ExtensionsListView implements IWorkspaceRecommendedExtensionsView { private readonly recommendedExtensionsQuery = '@recommended:workspace'; - private installAllAction: Action | undefined; renderBody(container: HTMLElement): void { super.renderBody(container); @@ -1085,31 +1086,13 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { this._register(this.contextService.onDidChangeWorkbenchState(() => this.show(this.recommendedExtensionsQuery))); } - getActions(): IAction[] { - if (!this.installAllAction) { - this.installAllAction = this._register(new Action('workbench.extensions.action.installWorkspaceRecommendedExtensions', localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), ThemeIcon.asClassName(installWorkspaceRecommendedIcon), false, () => this.installWorkspaceRecommendations())); - } - - const configureWorkspaceFolderAction = this._register(this.instantiationService.createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL)); - configureWorkspaceFolderAction.class = ThemeIcon.asClassName(configureRecommendedIcon); - return [this.installAllAction, configureWorkspaceFolderAction]; - } - async show(query: string): Promise> { let shouldShowEmptyView = query && query.trim() !== '@recommended' && query.trim() !== '@recommended:workspace'; let model = await (shouldShowEmptyView ? this.showEmptyModel() : super.show(this.recommendedExtensionsQuery)); this.setExpanded(model.length > 0); - await this.setRecommendationsToInstall(); return model; } - private async setRecommendationsToInstall(): Promise { - const installableRecommendations = await this.getInstallableWorkspaceRecommendations(); - if (this.installAllAction) { - this.installAllAction.enabled = installableRecommendations.length > 0; - } - } - private async getInstallableWorkspaceRecommendations() { const installed = (await this.extensionsWorkbenchService.queryLocal()) .filter(l => l.enablementState !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind @@ -1118,9 +1101,16 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { return this.getInstallableRecommendations(recommendations, { source: 'install-all-workspace-recommendations' }, CancellationToken.None); } - private async installWorkspaceRecommendations(): Promise { + async installWorkspaceRecommendations(): Promise { const installableRecommendations = await this.getInstallableWorkspaceRecommendations(); - await this.extensionManagementService.installExtensions(installableRecommendations.map(i => i.gallery!)); + if (installableRecommendations.length) { + await this.extensionManagementService.installExtensions(installableRecommendations.map(i => i.gallery!)); + } else { + this.notificationService.notify({ + severity: Severity.Info, + message: localize('no local extensions', "There are no extensions to install.") + }); + } } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 3566ca41..2f8ac0a8 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -22,7 +22,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { URI } from 'vs/base/common/uri'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext } from 'vs/workbench/contrib/extensions/common/extensions'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IURLService, IURLHandler, IOpenURLOptions } from 'vs/platform/url/common/url'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; @@ -40,6 +40,7 @@ import { getExtensionKind } from 'vs/workbench/services/extensions/common/extens import { FileAccess } from 'vs/base/common/network'; import { IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; interface IExtensionStateProvider { (extension: Extension): T; @@ -492,6 +493,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private static readonly SyncPeriod = 1000 * 60 * 60 * 12; // 12 hours declare readonly _serviceBrand: undefined; + private hasOutdatedExtensionsContextKey: IContextKey; + private readonly localExtensions: Extensions | null = null; private readonly remoteExtensions: Extensions | null = null; private readonly webExtensions: Extensions | null = null; @@ -520,9 +523,11 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension @IModeService private readonly modeService: IModeService, @IIgnoredExtensionsManagementService private readonly extensionsSyncManagementService: IIgnoredExtensionsManagementService, @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, - @IProductService private readonly productService: IProductService + @IProductService private readonly productService: IProductService, + @IContextKeyService contextKeyService: IContextKeyService ) { super(); + this.hasOutdatedExtensionsContextKey = HasOutdatedExtensionsContext.bindTo(contextKeyService); if (extensionManagementServerService.localExtensionManagementServer) { this.localExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.localExtensionManagementServer, ext => this.getExtensionState(ext))); this._register(this.localExtensions.onChange(e => this._onChange.fire(e ? e.extension : undefined))); @@ -559,7 +564,10 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this.eventuallySyncWithGallery(true); }); - this._register(this.onChange(() => this.updateActivity())); + this._register(this.onChange(() => { + this.updateContexts(); + this.updateActivity(); + })); } get local(): IExtension[] { @@ -1189,13 +1197,21 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return changed; } + private updateContexts(extension?: Extension): void { + if (extension && extension.outdated) { + this.hasOutdatedExtensionsContextKey.set(true); + } else { + this.hasOutdatedExtensionsContextKey.set(this.outdated.length > 0); + } + } + private _activityCallBack: ((value: void) => void) | null = null; private updateActivity(): void { if ((this.localExtensions && this.localExtensions.local.some(e => e.state === ExtensionState.Installing || e.state === ExtensionState.Uninstalling)) || (this.remoteExtensions && this.remoteExtensions.local.some(e => e.state === ExtensionState.Installing || e.state === ExtensionState.Uninstalling)) || (this.webExtensions && this.webExtensions.local.some(e => e.state === ExtensionState.Installing || e.state === ExtensionState.Uninstalling))) { if (!this._activityCallBack) { - this.progressService.withProgress({ location: ProgressLocation.Extensions }, () => new Promise(c => this._activityCallBack = c)); + this.progressService.withProgress({ location: ProgressLocation.Extensions }, () => new Promise(resolve => this._activityCallBack = resolve)); } } else { if (this._activityCallBack) { diff --git a/src/vs/workbench/contrib/extensions/browser/media/extension.css b/src/vs/workbench/contrib/extensions/browser/media/extension.css index 0502ce08..8749cbe2 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extension.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extension.css @@ -179,3 +179,7 @@ .extension-list-item > .details > .footer > .monaco-action-bar > .actions-container .extension-action.label { max-width: 150px; } + +.extension-list-item .footer .monaco-action-bar .action-item.action-dropdown-item.empty > .action-label { + margin-right: 4px; +} diff --git a/src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts b/src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts deleted file mode 100644 index 3a8caf10..00000000 --- a/src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts +++ /dev/null @@ -1,59 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { MenuRegistry, MenuId, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; -import { localize } from 'vs/nls'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { InstallLocalExtensionsInRemoteAction, InstallRemoteExtensionsInLocalAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; -import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; - -export class RemoteExtensionsInstaller extends Disposable implements IWorkbenchContribution { - - constructor( - @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, - @ILabelService labelService: ILabelService, - @IInstantiationService instantiationService: IInstantiationService - ) { - super(); - if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { - const installLocalExtensionsInRemoteAction = instantiationService.createInstance(InstallLocalExtensionsInRemoteAction); - CommandsRegistry.registerCommand('workbench.extensions.installLocalExtensions', () => installLocalExtensionsInRemoteAction.run()); - let disposable = Disposable.None; - const appendMenuItem = () => { - disposable.dispose(); - disposable = MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: 'workbench.extensions.installLocalExtensions', - category: localize({ key: 'remote', comment: ['Remote as in remote machine'] }, "Remote"), - title: installLocalExtensionsInRemoteAction.label - } - }); - }; - appendMenuItem(); - this._register(labelService.onDidChangeFormatters(e => appendMenuItem())); - this._register(toDisposable(() => disposable.dispose())); - - this._register(registerAction2(class InstallRemoteExtensionsInLocalAction2 extends Action2 { - constructor() { - super({ - id: 'workbench.extensions.actions.installLocalExtensionsInRemote', - title: { value: localize('install remote in local', "Install Remote Extensions Locally..."), original: 'Install Remote Extensions Locally...' }, - category: localize({ key: 'remote', comment: ['Remote as in remote machine'] }, "Remote"), - f1: true - }); - } - run(accessor: ServicesAccessor): Promise { - return accessor.get(IInstantiationService).createInstance(InstallRemoteExtensionsInLocalAction, 'workbench.extensions.actions.installLocalExtensionsInRemote').run(); - } - })); - } - } - -} diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 3cfe2796..127ae556 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -13,15 +13,21 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionManifest, ExtensionType } from 'vs/platform/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; -import { IViewPaneContainer } from 'vs/workbench/common/views'; +import { IView, IViewPaneContainer } from 'vs/workbench/common/views'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export const VIEWLET_ID = 'workbench.view.extensions'; export interface IExtensionsViewPaneContainer extends IViewPaneContainer { + readonly searchValue: string | undefined; search(text: string): void; refresh(): Promise; } +export interface IWorkspaceRecommendedExtensionsView extends IView { + installWorkspaceRecommendations(): Promise; +} + export const enum ExtensionState { Installing, Installed, @@ -145,5 +151,12 @@ export class ExtensionContainers extends Disposable { } } +export const WORKSPACE_RECOMMENDATIONS_VIEW_ID = 'workbench.views.extensions.workspaceRecommendations'; export const TOGGLE_IGNORE_EXTENSION_ACTION_ID = 'workbench.extensions.action.toggleIgnoreExtension'; +export const SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID = 'workbench.extensions.action.installVSIX'; export const INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID = 'workbench.extensions.command.installFromVSIX'; + +// Context Keys +export const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); +export const ExtensionsSortByContext = new RawContextKey('extensionsSortByValue', ''); +export const HasOutdatedExtensionsContext = new RawContextKey('hasOutdatedExtensions', false); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts index a472654f..edf6d100 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts @@ -21,7 +21,6 @@ import { ExtensionHostProfileService } from 'vs/workbench/contrib/extensions/ele import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionsAutoProfiler } from 'vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler'; -import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { OpenExtensionsFolderAction } from 'vs/workbench/contrib/extensions/electron-sandbox/extensionsActions'; import { ExtensionsLabel } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; @@ -62,11 +61,10 @@ const actionRegistry = Registry.as(WorkbenchActionExte class ExtensionsContributions implements IWorkbenchContribution { constructor( - @INativeWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService, @IExtensionRecommendationNotificationService extensionRecommendationNotificationService: IExtensionRecommendationNotificationService, @ISharedProcessService sharedProcessService: ISharedProcessService, ) { - sharedProcessService.registerChannel('IExtensionRecommendationNotificationService', new ExtensionRecommendationNotificationServiceChannel(extensionRecommendationNotificationService)); + sharedProcessService.registerChannel('extensionRecommendationNotification', new ExtensionRecommendationNotificationServiceChannel(extensionRecommendationNotificationService)); const openExtensionsFolderActionDescriptor = SyncActionDescriptor.from(OpenExtensionsFolderAction); actionRegistry.registerWorkbenchAction(openExtensionsFolderActionDescriptor, 'Extensions: Open Extensions Folder', ExtensionsLabel); } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/reportExtensionIssueAction.ts b/src/vs/workbench/contrib/extensions/electron-browser/reportExtensionIssueAction.ts index faf606a7..762b07c4 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/reportExtensionIssueAction.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/reportExtensionIssueAction.ts @@ -14,6 +14,8 @@ import { ExtensionType, IExtensionDescription } from 'vs/platform/extensions/com import { IOpenerService } from 'vs/platform/opener/common/opener'; import { URI } from 'vs/base/common/uri'; +const builtinExtensionIssueUrl = 'https://github.com/microsoft/vscode'; + export class ReportExtensionIssueAction extends Action { private static readonly _id = 'workbench.extensions.action.reportExtensionIssue'; @@ -34,7 +36,8 @@ export class ReportExtensionIssueAction extends Action { @INativeHostService private readonly nativeHostService: INativeHostService ) { super(ReportExtensionIssueAction._id, ReportExtensionIssueAction._label, 'extension-action report-issue'); - this.enabled = !!extension.description.repository && !!extension.description.repository.url; + + this.enabled = extension.description.isBuiltin || (!!extension.description.repository && !!extension.description.repository.url); } async run(): Promise { @@ -51,6 +54,9 @@ export class ReportExtensionIssueAction extends Action { unresponsiveProfile?: IExtensionHostProfile }): Promise { let baseUrl = extension.marketplaceInfo && extension.marketplaceInfo.type === ExtensionType.User && extension.description.repository ? extension.description.repository.url : undefined; + if (!baseUrl && extension.description.isBuiltin) { + baseUrl = builtinExtensionIssueUrl; + } if (!!baseUrl) { baseUrl = `${baseUrl.indexOf('.git') !== -1 ? baseUrl.substr(0, baseUrl.length - 4) : baseUrl}/issues/new/`; } else { diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts index ed8a92a9..c380c4c7 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts @@ -5,11 +5,7 @@ import * as sinon from 'sinon'; import * as assert from 'assert'; -import * as path from 'vs/base/common/path'; -import * as fs from 'fs'; -import * as os from 'os'; import * as uuid from 'vs/base/common/uuid'; -import { mkdirp, rimraf, RimRafMode } from 'vs/base/node/pfs'; import { IExtensionGalleryService, IGalleryExtensionAssets, IGalleryExtension, IExtensionManagementService, DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, IExtensionTipsService @@ -47,8 +43,6 @@ import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { FileService } from 'vs/platform/files/common/fileService'; import { NullLogService, ILogService } from 'vs/platform/log/common/log'; -import { Schemas } from 'vs/base/common/network'; -import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { IFileService } from 'vs/platform/files/common/files'; import { IProductService } from 'vs/platform/product/common/productService'; import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-sandbox/extensionTipsService'; @@ -62,6 +56,11 @@ import { IExtensionIgnoredRecommendationsService } from 'vs/workbench/services/e import { ExtensionIgnoredRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionIgnoredRecommendationsService'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { ExtensionRecommendationNotificationService } from 'vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { joinPath } from 'vs/base/common/resources'; +import { VSBuffer } from 'vs/base/common/buffer'; const mockExtensionGallery: IGalleryExtension[] = [ aGalleryExtension('MockExtension1', { @@ -181,7 +180,6 @@ suite('ExtensionRecommendationsService Test', () => { let instantiationService: TestInstantiationService; let testConfigurationService: TestConfigurationService; let testObject: ExtensionRecommendationsService; - let parentResource: string; let installEvent: Emitter, didInstallEvent: Emitter, uninstallEvent: Emitter, @@ -203,6 +201,7 @@ suite('ExtensionRecommendationsService Test', () => { testConfigurationService = new TestConfigurationService(); instantiationService.stub(IConfigurationService, testConfigurationService); instantiationService.stub(INotificationService, new TestNotificationService()); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IExtensionManagementService, >{ onInstallExtension: installEvent.event, onDidInstallExtension: didInstallEvent.event, @@ -278,34 +277,30 @@ suite('ExtensionRecommendationsService Test', () => { }); }); - teardown(done => { - (testObject).dispose(); - if (parentResource) { - rimraf(parentResource, RimRafMode.MOVE).then(done, done); - } else { - done(); - } - }); + teardown(() => (testObject).dispose()); function setUpFolderWorkspace(folderName: string, recommendedExtensions: string[], ignoredRecommendations: string[] = []): Promise { - const id = uuid.generateUuid(); - parentResource = path.join(os.tmpdir(), 'vsctests', id); - return setUpFolder(folderName, parentResource, recommendedExtensions, ignoredRecommendations); + return setUpFolder(folderName, recommendedExtensions, ignoredRecommendations); } - async function setUpFolder(folderName: string, parentDir: string, recommendedExtensions: string[], ignoredRecommendations: string[] = []): Promise { - const folderDir = path.join(parentDir, folderName); - const workspaceSettingsDir = path.join(folderDir, '.vscode'); - await mkdirp(workspaceSettingsDir, 493); - const configPath = path.join(workspaceSettingsDir, 'extensions.json'); - fs.writeFileSync(configPath, JSON.stringify({ + async function setUpFolder(folderName: string, recommendedExtensions: string[], ignoredRecommendations: string[] = []): Promise { + const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); + const logService = new NullLogService(); + const fileService = new FileService(logService); + const fileSystemProvider = new InMemoryFileSystemProvider(); + fileService.registerProvider(ROOT.scheme, fileSystemProvider); + + const folderDir = joinPath(ROOT, folderName); + const workspaceSettingsDir = joinPath(folderDir, '.vscode'); + await fileService.createFolder(workspaceSettingsDir); + const configPath = joinPath(workspaceSettingsDir, 'extensions.json'); + await fileService.writeFile(configPath, VSBuffer.fromString(JSON.stringify({ 'recommendations': recommendedExtensions, 'unwantedRecommendations': ignoredRecommendations, - }, null, '\t')); + }, null, '\t'))); + + const myWorkspace = testWorkspace(folderDir); - const myWorkspace = testWorkspace(URI.from({ scheme: 'file', path: folderDir })); - const fileService = new FileService(new NullLogService()); - fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService())); instantiationService.stub(IFileService, fileService); workspaceService = new TestContextService(myWorkspace); instantiationService.stub(IWorkspaceContextService, workspaceService); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts index 40cf65cc..bcef66bb 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts @@ -33,7 +33,7 @@ import { NativeURLService } from 'vs/platform/url/common/urlService'; import { URI } from 'vs/base/common/uri'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; +import { RemoteAgentService } from 'vs/workbench/services/remote/electron-sandbox/remoteAgentServiceImpl'; import { ExtensionIdentifier, IExtensionContributions, ExtensionType, IExtensionDescription, IExtension } from 'vs/platform/extensions/common/extensions'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -53,6 +53,8 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { UserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; import { IUserDataAutoSyncEnablementService, IUserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSyncResourceEnablementService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; let instantiationService: TestInstantiationService; let installEvent: Emitter, @@ -77,6 +79,7 @@ async function setupTest() { instantiationService.stub(IConfigurationService, new TestConfigurationService()); instantiationService.stub(IProgressService, ProgressService); instantiationService.stub(IProductService, {}); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IExtensionGalleryService, ExtensionGalleryService); instantiationService.stub(ISharedProcessService, TestSharedProcessService); @@ -885,83 +888,6 @@ suite('ExtensionsActions', () => { }); }); - test('Test UpdateAllAction when no installed extensions', () => { - const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label', true); - - assert.ok(!testObject.enabled); - }); - - test('Test UpdateAllAction when installed extensions are not outdated', () => { - const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label', true); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [aLocalExtension('a'), aLocalExtension('b')]); - return instantiationService.get(IExtensionsWorkbenchService).queryLocal() - .then(extensions => assert.ok(!testObject.enabled)); - }); - - test('Test UpdateAllAction when some installed extensions are outdated', () => { - const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label', true); - const local = [aLocalExtension('a', { version: '1.0.1' }), aLocalExtension('b', { version: '1.0.1' }), aLocalExtension('c', { version: '1.0.1' })]; - const workbenchService = instantiationService.get(IExtensionsWorkbenchService); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', local); - return workbenchService.queryLocal() - .then(async () => { - instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: local[0].identifier, version: '1.0.2' }), aGalleryExtension('b', { identifier: local[1].identifier, version: '1.0.2' }), aGalleryExtension('c', local[2].manifest))); - assert.ok(!testObject.enabled); - return new Promise(c => { - testObject.onDidChange(() => { - if (testObject.enabled) { - c(); - } - }); - workbenchService.queryGallery(CancellationToken.None); - }); - }); - }); - - test('Test UpdateAllAction when some installed extensions are outdated and some outdated are being installed', () => { - const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label', true); - const local = [aLocalExtension('a', { version: '1.0.1' }), aLocalExtension('b', { version: '1.0.1' }), aLocalExtension('c', { version: '1.0.1' })]; - const gallery = [aGalleryExtension('a', { identifier: local[0].identifier, version: '1.0.2' }), aGalleryExtension('b', { identifier: local[1].identifier, version: '1.0.2' }), aGalleryExtension('c', local[2].manifest)]; - const workbenchService = instantiationService.get(IExtensionsWorkbenchService); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', local); - return workbenchService.queryLocal() - .then(async () => { - instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...gallery)); - assert.ok(!testObject.enabled); - return new Promise(c => { - installEvent.fire({ identifier: local[0].identifier, gallery: gallery[0] }); - testObject.onDidChange(() => { - if (testObject.enabled) { - c(); - } - }); - workbenchService.queryGallery(CancellationToken.None); - }); - }); - }); - - test('Test UpdateAllAction when some installed extensions are outdated and all outdated are being installed', () => { - const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label', true); - const local = [aLocalExtension('a', { version: '1.0.1' }), aLocalExtension('b', { version: '1.0.1' }), aLocalExtension('c', { version: '1.0.1' })]; - const gallery = [aGalleryExtension('a', { identifier: local[0].identifier, version: '1.0.2' }), aGalleryExtension('b', { identifier: local[1].identifier, version: '1.0.2' }), aGalleryExtension('c', local[2].manifest)]; - const workbenchService = instantiationService.get(IExtensionsWorkbenchService); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', local); - return workbenchService.queryLocal() - .then(() => { - instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...gallery)); - return workbenchService.queryGallery(CancellationToken.None) - .then(() => { - installEvent.fire({ identifier: local[0].identifier, gallery: gallery[0] }); - installEvent.fire({ identifier: local[1].identifier, gallery: gallery[1] }); - assert.ok(!testObject.enabled); - }); - }); - }); - - test(`RecommendToFolderAction`, () => { - // TODO: Implement test - }); - }); suite('ReloadAction', () => { 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 cde013f4..b1e00bb8 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 @@ -35,7 +35,7 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { SinonStub } from 'sinon'; import { IExperimentService, ExperimentState, ExperimentActionType, ExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; +import { RemoteAgentService } from 'vs/workbench/services/remote/electron-sandbox/remoteAgentServiceImpl'; import { ExtensionType, IExtension, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -165,7 +165,8 @@ suite('ExtensionsListView Tests', () => { instantiationService.stub(IViewDescriptorService, { getViewLocationById(): ViewContainerLocation { return ViewContainerLocation.Sidebar; - } + }, + onDidChangeLocation: Event.None }); instantiationService.stub(IExtensionService, >{ diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index f4f5d8ee..76a50206 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -36,7 +36,7 @@ import { URI } from 'vs/base/common/uri'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtensionType, IExtension, ExtensionKind } from 'vs/platform/extensions/common/extensions'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; +import { RemoteAgentService } from 'vs/workbench/services/remote/electron-sandbox/remoteAgentServiceImpl'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -46,6 +46,8 @@ import { IExperimentService } from 'vs/workbench/contrib/experiments/common/expe import { TestExperimentService } from 'vs/workbench/contrib/experiments/test/electron-browser/experimentService.test'; import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-sandbox/extensionTipsService'; import { Schemas } from 'vs/base/common/network'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; suite('ExtensionsWorkbenchServiceTest', () => { @@ -72,6 +74,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { instantiationService.stub(IExtensionGalleryService, ExtensionGalleryService); instantiationService.stub(IURLService, NativeURLService); instantiationService.stub(ISharedProcessService, TestSharedProcessService); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IWorkspaceContextService, new TestContextService()); instantiationService.stub(IConfigurationService, >{ diff --git a/src/vs/workbench/contrib/externalUriOpener/common/configuration.ts b/src/vs/workbench/contrib/externalUriOpener/common/configuration.ts new file mode 100644 index 00000000..83bd5a39 --- /dev/null +++ b/src/vs/workbench/contrib/externalUriOpener/common/configuration.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IConfigurationNode, IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; +import * as nls from 'vs/nls'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { Registry } from 'vs/platform/registry/common/platform'; + +export const defaultExternalUriOpenerId = 'default'; + +export const externalUriOpenersSettingId = 'workbench.externalUriOpeners'; + +export interface ExternalUriOpenersConfiguration { + readonly [uriGlob: string]: string; +} + +const externalUriOpenerIdSchemaAddition: IJSONSchema = { + type: 'string', + enum: [] +}; + +const exampleUriPatterns = ` +- \`https://microsoft.com\`: Matches this specific domain using https +- \`https://microsoft.com:8080\`: Matches this specific domain on this port using https +- \`https://microsoft.com:*\`: Matches this specific domain on any port using https +- \`https://microsoft.com/foo\`: Matches \`https://microsoft.com/foo\` and \`https://microsoft.com/foo/bar\`, but not \`https://microsoft.com/foobar\` or \`https://microsoft.com/bar\` +- \`https://*.microsoft.com\`: Match all domains ending in \`microsoft.com\` using https +- \`microsoft.com\`: Match this specific domain using either http or https +- \`*.microsoft.com\`: Match all domains ending in \`microsoft.com\` using either http or https +- \`http://192.168.0.1\`: Matches this specific IP using http +- \`http://192.168.0.*\`: Matches all IP's with this prefix using http +- \`*\`: Match all domains using either http or https`; + +export const externalUriOpenersConfigurationNode: IConfigurationNode = { + ...workbenchConfigurationNodeBase, + properties: { + [externalUriOpenersSettingId]: { + type: 'object', + markdownDescription: nls.localize('externalUriOpeners', "Configure the opener to use for external uris (i.e. http, https)."), + defaultSnippets: [{ + body: { + 'example.com': '$1' + } + }], + additionalProperties: { + anyOf: [ + { + type: 'string', + markdownDescription: nls.localize('externalUriOpeners.uri', "Map uri pattern to an opener id.\nExample patterns: \n{0}", exampleUriPatterns), + }, + { + type: 'string', + markdownDescription: nls.localize('externalUriOpeners.uri', "Map uri pattern to an opener id.\nExample patterns: \n{0}", exampleUriPatterns), + enum: [defaultExternalUriOpenerId], + enumDescriptions: [nls.localize('externalUriOpeners.defaultId', "Open using VS Code's standard opener.")], + }, + externalUriOpenerIdSchemaAddition + ] + } + } + } +}; + +export function updateContributedOpeners(enumValues: string[], enumDescriptions: string[]): void { + externalUriOpenerIdSchemaAddition.enum = enumValues; + externalUriOpenerIdSchemaAddition.enumDescriptions = enumDescriptions; + + Registry.as(Extensions.Configuration) + .notifyConfigurationSchemaUpdated(externalUriOpenersConfigurationNode); +} diff --git a/src/vs/workbench/contrib/externalUriOpener/common/contributedOpeners.ts b/src/vs/workbench/contrib/externalUriOpener/common/contributedOpeners.ts new file mode 100644 index 00000000..457d5df3 --- /dev/null +++ b/src/vs/workbench/contrib/externalUriOpener/common/contributedOpeners.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { Memento } from 'vs/workbench/common/memento'; +import { updateContributedOpeners } from 'vs/workbench/contrib/externalUriOpener/common/configuration'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +interface RegisteredExternalOpener { + readonly extensionId: string; +} + +interface OpenersMemento { + [id: string]: RegisteredExternalOpener; +} + +/** + */ +export class ContributedExternalUriOpenersStore extends Disposable { + + private static readonly STORAGE_ID = 'externalUriOpeners'; + + private readonly _openers = new Map(); + private readonly _memento: Memento; + private _mementoObject: OpenersMemento; + + constructor( + @IStorageService storageService: IStorageService, + @IExtensionService private readonly _extensionService: IExtensionService + ) { + super(); + + this._memento = new Memento(ContributedExternalUriOpenersStore.STORAGE_ID, storageService); + this._mementoObject = this._memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + for (const id of Object.keys(this._mementoObject || {})) { + this.add(id, this._mementoObject[id].extensionId); + } + + this.invalidateOpenersForUninstalledExtension(); + + this._register(this._extensionService.onDidChangeExtensions(() => this.invalidateOpenersForUninstalledExtension())); + } + + public add(id: string, extensionId: string): void { + this._openers.set(id, { extensionId }); + + this._mementoObject[id] = { extensionId }; + this._memento.saveMemento(); + + this.updateSchema(); + } + + public delete(id: string): void { + this._openers.delete(id); + + delete this._mementoObject[id]; + this._memento.saveMemento(); + + this.updateSchema(); + } + + private async invalidateOpenersForUninstalledExtension() { + const registeredExtensions = await this._extensionService.getExtensions(); + for (const [id, entry] of this._openers) { + const isExtensionRegistered = registeredExtensions.some(r => r.identifier.value === entry.extensionId); + if (!isExtensionRegistered) { + this.delete(id); + } + } + } + + private updateSchema() { + const ids: string[] = []; + const descriptions: string[] = []; + + for (const [id, entry] of this._openers) { + ids.push(id); + descriptions.push(entry.extensionId); + } + + updateContributedOpeners(ids, descriptions); + } +} diff --git a/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpener.contribution.ts b/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpener.contribution.ts new file mode 100644 index 00000000..34f349d2 --- /dev/null +++ b/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpener.contribution.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { externalUriOpenersConfigurationNode } from 'vs/workbench/contrib/externalUriOpener/common/configuration'; +import { ExternalUriOpenerService, IExternalUriOpenerService } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService'; + +registerSingleton(IExternalUriOpenerService, ExternalUriOpenerService); + +Registry.as(ConfigurationExtensions.Configuration) + .registerConfiguration(externalUriOpenersConfigurationNode); diff --git a/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService.ts b/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService.ts new file mode 100644 index 00000000..069ad5e4 --- /dev/null +++ b/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService.ts @@ -0,0 +1,216 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { firstOrDefault } from 'vs/base/common/arrays'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Iterable } from 'vs/base/common/iterator'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { LinkedList } from 'vs/base/common/linkedList'; +import { isWeb } from 'vs/base/common/platform'; +import { URI } from 'vs/base/common/uri'; +import * as modes from 'vs/editor/common/modes'; +import * as nls from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IExternalOpener, IOpenerService } from 'vs/platform/opener/common/opener'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { defaultExternalUriOpenerId, ExternalUriOpenersConfiguration, externalUriOpenersSettingId } from 'vs/workbench/contrib/externalUriOpener/common/configuration'; +import { testUrlMatchesGlob } from 'vs/workbench/contrib/url/common/urlGlob'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; + + +export const IExternalUriOpenerService = createDecorator('externalUriOpenerService'); + + +export interface IExternalOpenerProvider { + getOpeners(targetUri: URI): AsyncIterable; +} + +export interface IExternalUriOpener { + readonly id: string; + readonly label: string; + + canOpen(uri: URI, token: CancellationToken): Promise; + openExternalUri(uri: URI, ctx: { sourceUri: URI }, token: CancellationToken): Promise; +} + +export interface IExternalUriOpenerService { + readonly _serviceBrand: undefined + + /** + * Registers a provider for external resources openers. + */ + registerExternalOpenerProvider(provider: IExternalOpenerProvider): IDisposable; +} + +export class ExternalUriOpenerService extends Disposable implements IExternalUriOpenerService, IExternalOpener { + + public readonly _serviceBrand: undefined; + + private readonly _providers = new LinkedList(); + + constructor( + @IOpenerService openerService: IOpenerService, + @IStorageService storageService: IStorageService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @ILogService private readonly logService: ILogService, + @IPreferencesService private readonly preferencesService: IPreferencesService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + ) { + super(); + this._register(openerService.registerExternalOpener(this)); + } + + registerExternalOpenerProvider(provider: IExternalOpenerProvider): IDisposable { + const remove = this._providers.push(provider); + return { dispose: remove }; + } + + async openExternal(href: string, ctx: { sourceUri: URI, preferredOpenerId?: string }, token: CancellationToken): Promise { + + const targetUri = typeof href === 'string' ? URI.parse(href) : href; + + const allOpeners = await this.getAllOpenersForUri(targetUri); + + if (allOpeners.size === 0) { + return false; + } + + // First see if we have a preferredOpener + if (ctx.preferredOpenerId) { + if (ctx.preferredOpenerId === defaultExternalUriOpenerId) { + return false; + } + + const preferredOpener = allOpeners.get(ctx.preferredOpenerId); + if (preferredOpener) { + // Skip the `canOpen` check here since the opener was specifically requested. + return preferredOpener.openExternalUri(targetUri, ctx, token); + } + } + + // Check to see if we have a configured opener + const configuredOpener = this.getConfiguredOpenerForUri(allOpeners, targetUri); + if (configuredOpener) { + // Skip the `canOpen` check here since the opener was specifically requested. + return configuredOpener === defaultExternalUriOpenerId ? false : configuredOpener.openExternalUri(targetUri, ctx, token); + } + + // Then check to see if there is a valid opener + const validOpeners: Array<{ opener: IExternalUriOpener, priority: modes.ExternalUriOpenerPriority }> = []; + await Promise.all(Array.from(allOpeners.values()).map(async opener => { + let priority: modes.ExternalUriOpenerPriority; + try { + priority = await opener.canOpen(targetUri, token); + } catch (e) { + this.logService.error(e); + return; + } + + switch (priority) { + case modes.ExternalUriOpenerPriority.Option: + case modes.ExternalUriOpenerPriority.Default: + case modes.ExternalUriOpenerPriority.Preferred: + validOpeners.push({ opener, priority }); + break; + } + })); + + if (validOpeners.length === 0) { + return false; + } + + // See if we have a preferred opener first + const preferred = firstOrDefault(validOpeners.filter(x => x.priority === modes.ExternalUriOpenerPriority.Preferred)); + if (preferred) { + return preferred.opener.openExternalUri(targetUri, ctx, token); + } + + // See if we only have optional openers, use the default opener + if (validOpeners.every(x => x.priority === modes.ExternalUriOpenerPriority.Option)) { + return false; + } + + // Otherwise prompt + return this.showOpenerPrompt(validOpeners.map(x => x.opener), targetUri, ctx, token); + } + + private async getAllOpenersForUri(targetUri: URI): Promise> { + const allOpeners = new Map(); + await Promise.all(Iterable.map(this._providers, async (provider) => { + for await (const opener of provider.getOpeners(targetUri)) { + allOpeners.set(opener.id, opener); + } + })); + return allOpeners; + } + + private getConfiguredOpenerForUri(openers: Map, targetUri: URI): IExternalUriOpener | 'default' | undefined { + const config = this.configurationService.getValue(externalUriOpenersSettingId) || {}; + for (const [uriGlob, id] of Object.entries(config)) { + if (testUrlMatchesGlob(targetUri.toString(), uriGlob)) { + if (id === defaultExternalUriOpenerId) { + return 'default'; + } + + const entry = openers.get(id); + if (entry) { + return entry; + } + } + } + return undefined; + } + + private async showOpenerPrompt( + openers: ReadonlyArray, + targetUri: URI, + ctx: { sourceUri: URI }, + token: CancellationToken + ): Promise { + type PickItem = IQuickPickItem & { opener?: IExternalUriOpener | 'configureDefault' }; + + const items: Array = openers.map((opener): PickItem => { + return { + label: opener.label, + opener: opener + }; + }); + items.push( + { + label: isWeb + ? nls.localize('selectOpenerDefaultLabel.web', 'Open in new browser window') + : nls.localize('selectOpenerDefaultLabel', 'Open in default browser'), + opener: undefined + }, + { type: 'separator' }, + { + label: nls.localize('selectOpenerConfigureTitle', "Configure default opener..."), + opener: 'configureDefault' + }); + + const picked = await this.quickInputService.pick(items, { + placeHolder: nls.localize('selectOpenerPlaceHolder', "How would you like to open: {0}", targetUri.toString()) + }); + + if (!picked) { + // Still cancel the default opener here since we prompted the user + return true; + } + + if (typeof picked.opener === 'undefined') { + return false; // Fallback to default opener + } else if (picked.opener === 'configureDefault') { + await this.preferencesService.openGlobalSettings(true, { + revealSetting: { key: externalUriOpenersSettingId, edit: true } + }); + return true; + } else { + return picked.opener.openExternalUri(targetUri, ctx, token); + } + } +} diff --git a/src/vs/workbench/contrib/externalUriOpener/test/common/externalUriOpenerService.test.ts b/src/vs/workbench/contrib/externalUriOpener/test/common/externalUriOpenerService.test.ts new file mode 100644 index 00000000..557424f7 --- /dev/null +++ b/src/vs/workbench/contrib/externalUriOpener/test/common/externalUriOpenerService.test.ts @@ -0,0 +1,128 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { ExternalUriOpenerPriority } from 'vs/editor/common/modes'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IPickOptions, IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import { ExternalUriOpenerService, IExternalOpenerProvider, IExternalUriOpener } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService'; + + +class MockQuickInputService implements Partial{ + + constructor( + private readonly pickIndex: number + ) { } + + public pick(picks: Promise[]> | QuickPickInput[], options?: IPickOptions & { canPickMany: true }, token?: CancellationToken): Promise; + public pick(picks: Promise[]> | QuickPickInput[], options?: IPickOptions & { canPickMany: false }, token?: CancellationToken): Promise; + public async pick(picks: Promise[]> | QuickPickInput[], options?: Omit, 'canPickMany'>, token?: CancellationToken): Promise { + const resolvedPicks = await picks; + const item = resolvedPicks[this.pickIndex]; + if (item.type === 'separator') { + return undefined; + } + return item; + } + +} + +suite('ExternalUriOpenerService', () => { + + let instantiationService: TestInstantiationService; + + setup(() => { + instantiationService = new TestInstantiationService(); + + instantiationService.stub(IConfigurationService, new TestConfigurationService()); + instantiationService.stub(IOpenerService, { + registerExternalOpener: () => { return Disposable.None; } + }); + }); + + test('Should not open if there are no openers', async () => { + const externalUriOpenerService: ExternalUriOpenerService = instantiationService.createInstance(ExternalUriOpenerService); + + externalUriOpenerService.registerExternalOpenerProvider(new class implements IExternalOpenerProvider { + async *getOpeners(_targetUri: URI): AsyncGenerator { + // noop + } + }); + + const uri = URI.parse('http://contoso.com'); + const didOpen = await externalUriOpenerService.openExternal(uri.toString(), { sourceUri: uri }, CancellationToken.None); + assert.strictEqual(didOpen, false); + }); + + test('Should prompt if there is at least one enabled opener', async () => { + instantiationService.stub(IQuickInputService, new MockQuickInputService(0)); + + const externalUriOpenerService: ExternalUriOpenerService = instantiationService.createInstance(ExternalUriOpenerService); + + let openedWithEnabled = false; + externalUriOpenerService.registerExternalOpenerProvider(new class implements IExternalOpenerProvider { + async *getOpeners(_targetUri: URI): AsyncGenerator { + yield { + id: 'disabled-id', + label: 'disabled', + canOpen: async () => ExternalUriOpenerPriority.None, + openExternalUri: async () => true, + }; + yield { + id: 'enabled-id', + label: 'enabled', + canOpen: async () => ExternalUriOpenerPriority.Default, + openExternalUri: async () => { + openedWithEnabled = true; + return true; + } + }; + } + }); + + const uri = URI.parse('http://contoso.com'); + const didOpen = await externalUriOpenerService.openExternal(uri.toString(), { sourceUri: uri }, CancellationToken.None); + assert.strictEqual(didOpen, true); + assert.strictEqual(openedWithEnabled, true); + }); + + test('Should automatically pick single preferred opener without prompt', async () => { + const externalUriOpenerService: ExternalUriOpenerService = instantiationService.createInstance(ExternalUriOpenerService); + + let openedWithPreferred = false; + externalUriOpenerService.registerExternalOpenerProvider(new class implements IExternalOpenerProvider { + async *getOpeners(_targetUri: URI): AsyncGenerator { + yield { + id: 'other-id', + label: 'other', + canOpen: async () => ExternalUriOpenerPriority.Default, + openExternalUri: async () => { + return true; + } + }; + yield { + id: 'preferred-id', + label: 'preferred', + canOpen: async () => ExternalUriOpenerPriority.Preferred, + openExternalUri: async () => { + openedWithPreferred = true; + return true; + } + }; + } + }); + + const uri = URI.parse('http://contoso.com'); + const didOpen = await externalUriOpenerService.openExternal(uri.toString(), { sourceUri: uri }, CancellationToken.None); + assert.strictEqual(didOpen, true); + assert.strictEqual(openedWithPreferred, true); + }); +}); diff --git a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts index 7b93fdb2..1cc203a0 100644 --- a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts @@ -10,13 +10,11 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { BINARY_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { openEditorWith } from 'vs/workbench/services/editor/common/editorOpenWith'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; /** * An implementation of editor for binary files that cannot be displayed. @@ -29,9 +27,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IOpenerService private readonly openerService: IOpenerService, - @IEditorService private readonly editorService: IEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IQuickInputService private readonly quickInputService: IQuickInputService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, ) { @@ -55,7 +51,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { input.setForceOpenAsText(); // If more editors are installed that can handle this input, show a picker - await openEditorWith(input, undefined, options, this.group, this.editorService, this.configurationService, this.quickInputService); + await this.instantiationService.invokeFunction(openEditorWith, input, undefined, options, this.group); } } diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index ec837dc4..cd7fb061 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; +import { localize } from 'vs/nls'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { isFunction, assertIsDefined } from 'vs/base/common/types'; import { isValidBasename } from 'vs/base/common/extpath'; import { basename } from 'vs/base/common/resources'; -import { Action } from 'vs/base/common/actions'; +import { toAction } from 'vs/base/common/actions'; import { VIEWLET_ID, TEXT_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; import { ITextFileService, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; @@ -97,7 +97,7 @@ export class TextFileEditor extends BaseTextEditor { } getTitle(): string { - return this.input ? this.input.getName() : nls.localize('textFileEditor', "Text File Editor"); + return this.input ? this.input.getName() : localize('textFileEditor', "Text File Editor"); } get input(): FileEditorInput | undefined { @@ -169,22 +169,24 @@ export class TextFileEditor extends BaseTextEditor { if ((error).fileOperationResult === FileOperationResult.FILE_IS_DIRECTORY) { this.openAsFolder(input); - throw new Error(nls.localize('openFolderError', "File is a directory")); + throw new Error(localize('openFolderError', "File is a directory")); } // Offer to create a file from the error if we have a file not found and the name is valid if ((error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND && isValidBasename(basename(input.preferredResource))) { throw createErrorWithActions(toErrorMessage(error), { actions: [ - new Action('workbench.files.action.createMissingFile', nls.localize('createFile', "Create File"), undefined, true, async () => { - await this.textFileService.create(input.preferredResource); + toAction({ + id: 'workbench.files.action.createMissingFile', label: localize('createFile', "Create File"), run: async () => { + await this.textFileService.create([{ resource: input.preferredResource }]); - return this.editorService.openEditor({ - resource: input.preferredResource, - options: { - pinned: true // new file gets pinned by default - } - }); + return this.editorService.openEditor({ + resource: input.preferredResource, + options: { + pinned: true // new file gets pinned by default + } + }); + } }) ] }); diff --git a/src/vs/workbench/contrib/files/browser/explorerService.ts b/src/vs/workbench/contrib/files/browser/explorerService.ts index a1a53757..fa078a16 100644 --- a/src/vs/workbench/contrib/files/browser/explorerService.ts +++ b/src/vs/workbench/contrib/files/browser/explorerService.ts @@ -10,7 +10,7 @@ import { IFilesConfiguration, SortOrder } from 'vs/workbench/contrib/files/commo import { ExplorerItem, ExplorerModel } from 'vs/workbench/contrib/files/common/explorerModel'; import { URI } from 'vs/base/common/uri'; import { FileOperationEvent, FileOperation, IFileService, FileChangesEvent, FileChangeType, IResolveFileOptions } from 'vs/platform/files/common/files'; -import { dirname } from 'vs/base/common/resources'; +import { dirname, basename } from 'vs/base/common/resources'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -19,7 +19,7 @@ import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/ur import { IBulkEditService, ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; import { UndoRedoSource } from 'vs/platform/undoRedo/common/undoRedo'; import { IExplorerView, IExplorerService } from 'vs/workbench/contrib/files/browser/files'; -import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IProgressService, ProgressLocation, IProgressNotificationOptions, IProgressCompositeOptions } from 'vs/platform/progress/common/progress'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -28,7 +28,7 @@ export const UNDO_REDO_SOURCE = new UndoRedoSource(); export class ExplorerService implements IExplorerService { declare readonly _serviceBrand: undefined; - private static readonly EXPLORER_FILE_CHANGES_REACT_DELAY = 1000; // delay in ms to react to file changes to give our internal events a chance to react first + private static readonly EXPLORER_FILE_CHANGES_REACT_DELAY = 500; // delay in ms to react to file changes to give our internal events a chance to react first private readonly disposables = new DisposableStore(); private editable: { stat: ExplorerItem, data: IEditableData } | undefined; @@ -60,17 +60,32 @@ export class ExplorerService implements IExplorerService { this.fileChangeEvents = []; // Filter to the ones we care - const types = [FileChangeType.ADDED, FileChangeType.DELETED]; + const types = [FileChangeType.DELETED]; if (this._sortOrder === SortOrder.Modified) { types.push(FileChangeType.UPDATED); } let shouldRefresh = false; + // For DELETED and UPDATED events go through the explorer model and check if any of the items got affected this.roots.forEach(r => { if (this.view && !shouldRefresh) { shouldRefresh = doesFileEventAffect(r, this.view, events, types); } }); + // For ADDED events we need to go through all the events and check if the explorer is already aware of some of them + // Or if they affect not yet resolved parts of the explorer. If that is the case we will not refresh. + events.forEach(e => { + if (!shouldRefresh) { + const added = e.getAdded(); + if (added.some(a => { + const parent = this.model.findClosest(dirname(a.resource)); + // Parent of the added resource is resolved and the explorer model is not aware of the added resource - we need to refresh + return parent && !parent.getChild(basename(a.resource)); + })) { + shouldRefresh = true; + } + } + }); if (shouldRefresh) { await this.refresh(false); @@ -95,7 +110,7 @@ export class ExplorerService implements IExplorerService { }); if (affected) { if (this.view) { - await this.view.refresh(true); + await this.view.setTreeInput(); } } })); @@ -125,19 +140,20 @@ export class ExplorerService implements IExplorerService { return this.view.getContext(respectMultiSelection); } - async applyBulkEdit(edit: ResourceFileEdit[], options: { undoLabel: string, progressLabel: string }): Promise { + async applyBulkEdit(edit: ResourceFileEdit[], options: { undoLabel: string, progressLabel: string, confirmBeforeUndo?: boolean, progressLocation?: ProgressLocation.Explorer | ProgressLocation.Window }): Promise { const cancellationTokenSource = new CancellationTokenSource(); - const promise = this.progressService.withProgress({ - location: ProgressLocation.Window, - delay: 500, + const promise = this.progressService.withProgress({ + location: options.progressLocation || ProgressLocation.Window, title: options.progressLabel, - cancellable: edit.length > 1 // Only allow cancellation when there is more than one edit. Since cancelling will not actually stop the current edit that is in progress. + cancellable: edit.length > 1, // Only allow cancellation when there is more than one edit. Since cancelling will not actually stop the current edit that is in progress. + delay: 500, }, async progress => { await this.bulkEditService.apply(edit, { undoRedoSource: UNDO_REDO_SOURCE, label: options.undoLabel, progress, - token: cancellationTokenSource.token + token: cancellationTokenSource.token, + confirmBeforeUndo: options.confirmBeforeUndo }); }, () => cancellationTokenSource.cancel()); await this.progressService.withProgress({ location: ProgressLocation.Explorer, delay: 500 }, () => promise); @@ -345,11 +361,11 @@ export class ExplorerService implements IExplorerService { } function doesFileEventAffect(item: ExplorerItem, view: IExplorerView, events: FileChangesEvent[], types: FileChangeType[]): boolean { - if (events.some(e => e.affects(item.resource, ...types))) { - return true; - } for (let [_name, child] of item.children) { if (view.isItemVisible(child)) { + if (events.some(e => e.contains(child.resource, ...types))) { + return true; + } if (child.isDirectory && child.isDirectoryResolved) { if (doesFileEventAffect(child, view, events, types)) { return true; diff --git a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index 9745ec52..4753cc33 100644 --- a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts +++ b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts @@ -28,7 +28,8 @@ import { DelegatingEditorService } from 'vs/workbench/services/editor/browser/ed import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorPane } from 'vs/workbench/common/editor'; -import { ViewPane, ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; 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'; @@ -41,6 +42,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; const explorerViewIcon = registerIcon('explorer-view-icon', Codicon.files, localize('explorerViewIcon', 'View icon of the explorer view.')); +const openEditorsViewIcon = registerIcon('open-editors-view-icon', Codicon.book, localize('openEditorsIcon', 'View icon of the open editors view.')); export class ExplorerViewletViewsContribution extends Disposable implements IWorkbenchContribution { @@ -111,12 +113,13 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor id: OpenEditorsView.ID, name: OpenEditorsView.NAME, ctorDescriptor: new SyncDescriptor(OpenEditorsView), - containerIcon: explorerViewIcon, + containerIcon: openEditorsViewIcon, order: 0, when: OpenEditorsVisibleContext, canToggleVisibility: true, canMoveView: true, - collapsed: true, + collapsed: false, + hideByDefault: true, focusCommand: { id: 'workbench.files.action.focusOpenEditorsView', keybindings: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_E) } @@ -207,7 +210,7 @@ export class ExplorerViewPaneContainer extends ViewPaneContainer { let delay = 0; const config = this.configurationService.getValue(); - const delayEditorOpeningInOpenedEditors = !!config.workbench.editor.enablePreview; // No need to delay if preview is disabled + const delayEditorOpeningInOpenedEditors = !!config.workbench?.editor?.enablePreview; // No need to delay if preview is disabled const activeGroup = this.editorGroupService.activeGroup; if (delayEditorOpeningInOpenedEditors && group === activeGroup && !activeGroup.previewEditor) { diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 80deb826..c9b2dcd2 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -5,12 +5,12 @@ import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ToggleAutoSaveAction, FocusFilesExplorer, GlobalCompareResourcesAction, SaveAllAction, ShowActiveFileInExplorer, CollapseExplorerView, RefreshExplorerView, CompareWithClipboardAction, NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, TRIGGER_RENAME_LABEL, MOVE_FILE_TO_TRASH_LABEL, COPY_FILE_LABEL, PASTE_FILE_LABEL, FileCopiedContext, renameHandler, moveFileToTrashHandler, copyFileHandler, pasteFileHandler, deleteFileHandler, cutFileHandler, DOWNLOAD_COMMAND_ID, openFilePreserveFocusHandler, DOWNLOAD_LABEL, ShowOpenedFileInNewWindow } from 'vs/workbench/contrib/files/browser/fileActions'; +import { ToggleAutoSaveAction, FocusFilesExplorer, GlobalCompareResourcesAction, ShowActiveFileInExplorer, CompareWithClipboardAction, NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, TRIGGER_RENAME_LABEL, MOVE_FILE_TO_TRASH_LABEL, COPY_FILE_LABEL, PASTE_FILE_LABEL, FileCopiedContext, renameHandler, moveFileToTrashHandler, copyFileHandler, pasteFileHandler, deleteFileHandler, cutFileHandler, DOWNLOAD_COMMAND_ID, openFilePreserveFocusHandler, DOWNLOAD_LABEL, ShowOpenedFileInNewWindow } from 'vs/workbench/contrib/files/browser/fileActions'; import { revertLocalChangesCommand, acceptLocalChangesCommand, CONFLICT_RESOLUTION_CONTEXT } from 'vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler'; import { SyncActionDescriptor, MenuId, MenuRegistry, ILocalizedString } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; -import { openWindowCommand, COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_TO_SIDE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_COMMAND_ID, SAVE_FILE_LABEL, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID, OpenEditorsGroupContext, COMPARE_WITH_SAVED_COMMAND_ID, COMPARE_RESOURCE_COMMAND_ID, SELECT_FOR_COMPARE_COMMAND_ID, ResourceSelectedForCompareContext, OpenEditorsDirtyEditorContext, COMPARE_SELECTED_COMMAND_ID, REMOVE_ROOT_FOLDER_COMMAND_ID, REMOVE_ROOT_FOLDER_LABEL, SAVE_FILES_COMMAND_ID, COPY_RELATIVE_PATH_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_LABEL, newWindowCommand, OpenEditorsReadonlyEditorContext, OPEN_WITH_EXPLORER_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID, NEW_UNTITLED_FILE_LABEL } from 'vs/workbench/contrib/files/browser/fileCommands'; +import { openWindowCommand, COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_TO_SIDE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_COMMAND_ID, SAVE_FILE_LABEL, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID, OpenEditorsGroupContext, COMPARE_WITH_SAVED_COMMAND_ID, COMPARE_RESOURCE_COMMAND_ID, SELECT_FOR_COMPARE_COMMAND_ID, ResourceSelectedForCompareContext, OpenEditorsDirtyEditorContext, COMPARE_SELECTED_COMMAND_ID, REMOVE_ROOT_FOLDER_COMMAND_ID, REMOVE_ROOT_FOLDER_LABEL, SAVE_FILES_COMMAND_ID, COPY_RELATIVE_PATH_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_LABEL, newWindowCommand, OpenEditorsReadonlyEditorContext, OPEN_WITH_EXPLORER_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID, NEW_UNTITLED_FILE_LABEL, SAVE_ALL_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -36,12 +36,9 @@ import { Codicon } from 'vs/base/common/codicons'; const category = { value: nls.localize('filesCategory', "File"), original: 'File' }; const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(SaveAllAction, { primary: undefined, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_S }, win: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_S) } }), 'File: Save All', category.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(GlobalCompareResourcesAction), 'File: Compare Active File With...', category.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(FocusFilesExplorer), 'File: Focus on Files Explorer', category.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(ShowActiveFileInExplorer), 'File: Reveal Active File in Side Bar', category.value); -registry.registerWorkbenchAction(SyncActionDescriptor.from(CollapseExplorerView), 'File: Collapse Folders in Explorer', category.value); -registry.registerWorkbenchAction(SyncActionDescriptor.from(RefreshExplorerView), 'File: Refresh Explorer', category.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(CompareWithClipboardAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_C) }), 'File: Compare Active File with Clipboard', category.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleAutoSaveAction), 'File: Toggle Auto Save', category.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(ShowOpenedFileInNewWindow, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_O) }), 'File: Open Active File in New Window', category.value, EmptyWorkspaceSupportContext); @@ -612,7 +609,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { group: '4_save', command: { - id: SaveAllAction.ID, + id: SAVE_ALL_COMMAND_ID, title: nls.localize({ key: 'miSaveAll', comment: ['&& denotes a mnemonic'] }, "Save A&&ll"), precondition: DirtyWorkingCopiesContext }, diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 46aacd31..69d3c400 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -15,13 +15,12 @@ import { DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/com import { VIEWLET_ID, IFilesConfiguration, VIEW_ID } from 'vs/workbench/contrib/files/common/files'; import { ByteSize, IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; -import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/explorerViewlet'; import { IQuickInputService, ItemActivation } from 'vs/platform/quickinput/common/quickInput'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ITextModel } from 'vs/editor/common/model'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { REVEAL_IN_EXPLORER_COMMAND_ID, SAVE_ALL_COMMAND_ID, SAVE_ALL_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; +import { REVEAL_IN_EXPLORER_COMMAND_ID, SAVE_ALL_IN_GROUP_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -83,40 +82,6 @@ async function refreshIfSeparator(value: string, explorerService: IExplorerServi } } -/* New File */ -export class NewFileAction extends Action { - static readonly ID = 'workbench.files.action.createFileFromExplorer'; - static readonly LABEL = nls.localize('createNewFile', "New File"); - - constructor( - @ICommandService private commandService: ICommandService - ) { - super('explorer.newFile', NEW_FILE_LABEL); - this.class = 'explorer-action ' + Codicon.newFile.classNames; - } - - run(): Promise { - return this.commandService.executeCommand(NEW_FILE_COMMAND_ID); - } -} - -/* New Folder */ -export class NewFolderAction extends Action { - static readonly ID = 'workbench.files.action.createFolderFromExplorer'; - static readonly LABEL = nls.localize('createNewFolder', "New Folder"); - - constructor( - @ICommandService private commandService: ICommandService - ) { - super('explorer.newFolder', NEW_FOLDER_LABEL); - this.class = 'explorer-action ' + Codicon.newFolder.classNames; - } - - run(): Promise { - return this.commandService.executeCommand(NEW_FOLDER_COMMAND_ID); - } -} - async function deleteFiles(explorerService: IExplorerService, workingCopyFileService: IWorkingCopyFileService, dialogService: IDialogService, configurationService: IConfigurationService, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false): Promise { let primaryButton: string; if (useTrash) { @@ -168,6 +133,9 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer } let confirmation: IConfirmationResult; + // We do not support undo of folders, so in that case the delete action is irreversible + const deleteDetail = distinctElements.some(e => e.isDirectory) ? nls.localize('irreversible', "This action is irreversible!") : + distinctElements.length > 1 ? nls.localize('restorePlural', "You can restore these files using the Undo command") : nls.localize('restore', "You can restore this file using the Undo command"); // Check if we need to ask for confirmation at all if (skipConfirm || (useTrash && configurationService.getValue(CONFIRM_DELETE_SETTING_KEY) === false)) { @@ -199,7 +167,7 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer else { let { message, detail } = getDeleteMessage(distinctElements); detail += detail ? '\n' : ''; - detail += nls.localize('irreversible', "This action is irreversible!"); + detail += deleteDetail; confirmation = await dialogService.confirm({ message, detail, @@ -222,8 +190,8 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer try { const resourceFileEdits = distinctElements.map(e => new ResourceFileEdit(e.resource, undefined, { recursive: true, folder: e.isDirectory, skipTrashBin: !useTrash, maxSize: MAX_UNDO_FILE_SIZE })); const options = { - undoLabel: distinctElements.length > 1 ? nls.localize('deleteBulkEdit', "Delete {0} files", distinctElements.length) : nls.localize('deleteFileBulkEdit', "Delete {0}", distinctElements[0].name), - progressLabel: distinctElements.length > 1 ? nls.localize('deletingBulkEdit', "Deleting {0} files", distinctElements.length) : nls.localize('deletingFileBulkEdit', "Deleting {0}", distinctElements[0].name), + undoLabel: distinctElements.length > 1 ? nls.localize({ key: 'deleteBulkEdit', comment: ['Placeholder will be replaced by the number of files deleted'] }, "Delete {0} files", distinctElements.length) : nls.localize({ key: 'deleteFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file deleted'] }, "Delete {0}", distinctElements[0].name), + progressLabel: distinctElements.length > 1 ? nls.localize({ key: 'deletingBulkEdit', comment: ['Placeholder will be replaced by the number of files deleted'] }, "Deleting {0} files", distinctElements.length) : nls.localize({ key: 'deletingFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file deleted'] }, "Deleting {0}", distinctElements[0].name), }; await explorerService.applyBulkEdit(resourceFileEdits, options); } catch (error) { @@ -234,7 +202,7 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer 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!"); + detailMessage = deleteDetail; primaryButton = nls.localize({ key: 'deletePermanentlyButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete Permanently"); } else { errorMessage = toErrorMessage(error, false); @@ -411,6 +379,32 @@ export function incrementFileName(name: string, isFolder: boolean, incrementalNa return `${name.substr(0, lastIndexOfDot)}.1${name.substr(lastIndexOfDot)}`; } + // 123 => 124 + let noNameNoExtensionRegex = RegExp('(\\d+)$'); + if (!isFolder && lastIndexOfDot === -1 && name.match(noNameNoExtensionRegex)) { + return name.replace(noNameNoExtensionRegex, (match, g1?) => { + let number = parseInt(g1); + return number < maxNumber + ? String(number + 1).padStart(g1.length, '0') + : `${g1}.1`; + }); + } + + // file => file1 + // file1 => file2 + let noExtensionRegex = RegExp('(.*)(\d*)$'); + if (!isFolder && lastIndexOfDot === -1 && name.match(noExtensionRegex)) { + return name.replace(noExtensionRegex, (match, g1?, g2?) => { + let number = parseInt(g2); + if (isNaN(number)) { + number = 0; + } + return number < maxNumber + ? g1 + String(number + 1).padStart(g2.length, '0') + : `${g1}${g2}.1`; + }); + } + // folder.1=>folder.2 if (isFolder && name.match(/(\d+)$/)) { return name.replace(/(\d+)$/, (match, ...groups) => { @@ -560,20 +554,6 @@ export abstract class BaseSaveAllAction extends Action { } } -export class SaveAllAction extends BaseSaveAllAction { - - static readonly ID = 'workbench.action.files.saveAll'; - static readonly LABEL = SAVE_ALL_LABEL; - - get class(): string { - return 'explorer-action ' + Codicon.saveAll.classNames; - } - - protected doRun(): Promise { - return this.commandService.executeCommand(SAVE_ALL_COMMAND_ID); - } -} - export class SaveAllInGroupAction extends BaseSaveAllAction { static readonly ID = 'workbench.files.action.saveAllInGroup'; @@ -645,48 +625,6 @@ export class ShowActiveFileInExplorer extends Action { } } -export class CollapseExplorerView extends Action { - - static readonly ID = 'workbench.files.action.collapseExplorerFolders'; - static readonly LABEL = nls.localize('collapseExplorerFolders', "Collapse Folders in Explorer"); - - constructor(id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService, - @IExplorerService readonly explorerService: IExplorerService - ) { - super(id, label, 'explorer-action ' + Codicon.collapseAll.classNames); - } - - async run(): Promise { - const explorerViewlet = (await this.viewletService.openViewlet(VIEWLET_ID))?.getViewPaneContainer() as ExplorerViewPaneContainer; - const explorerView = explorerViewlet.getExplorerView(); - if (explorerView) { - explorerView.collapseAll(); - } - } -} - -export class RefreshExplorerView extends Action { - - static readonly ID = 'workbench.files.action.refreshFilesExplorer'; - static readonly LABEL = nls.localize('refreshExplorer', "Refresh Explorer"); - - - constructor( - id: string, label: string, - @IViewletService private readonly viewletService: IViewletService, - @IExplorerService private readonly explorerService: IExplorerService - ) { - super(id, label, 'explorer-action ' + Codicon.refresh.classNames); - } - - async run(): Promise { - await this.viewletService.openViewlet(VIEWLET_ID); - await this.explorerService.refresh(); - } -} - export class ShowOpenedFileInNewWindow extends Action { static readonly ID = 'workbench.action.files.showOpenedFileInNewWindow'; @@ -915,7 +853,8 @@ async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boole const resourceToCreate = resources.joinPath(folder.resource, value); await explorerService.applyBulkEdit([new ResourceFileEdit(undefined, resourceToCreate, { folder: isFolder })], { undoLabel: nls.localize('createBulkEdit', "Create {0}", value), - progressLabel: nls.localize('creatingBulkEdit', "Creating {0}", value) + progressLabel: nls.localize('creatingBulkEdit', "Creating {0}", value), + confirmBeforeUndo: true }); await refreshIfSeparator(value, explorerService); @@ -1242,7 +1181,7 @@ const downloadFileHandler = async (accessor: ServicesAccessor) => { const destination = await fileDialogService.showSaveDialog({ availableFileSystems: [Schemas.file], saveLabel: mnemonicButtonLabel(nls.localize('downloadButton', "Download")), - title: explorerItem.isDirectory ? nls.localize('downloadFolder', "Download Folder") : nls.localize('downloadFile', "Download File"), + title: nls.localize('chooseWhereToDownload', "Choose Where to Download"), defaultUri }); @@ -1250,6 +1189,7 @@ const downloadFileHandler = async (accessor: ServicesAccessor) => { await explorerService.applyBulkEdit([new ResourceFileEdit(explorerItem.resource, destination, { overwrite: true, copy: true })], { undoLabel: nls.localize('downloadBulkEdit', "Download {0}", explorerItem.name), progressLabel: nls.localize('downloadingBulkEdit', "Downloading {0}", explorerItem.name), + progressLocation: ProgressLocation.Explorer }); } else { cts.cancel(); // User canceled a download. In case there were multiple files selected we should cancel the remainder of the prompts #86100 @@ -1305,15 +1245,19 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => { if (pasteShouldMove) { const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target)); const options = { - progressLabel: sourceTargetPairs.length > 1 ? nls.localize('movingBulkEdit', "Moving {0} files", sourceTargetPairs.length) : nls.localize('movingFileBulkEdit', "Moving {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)), - undoLabel: sourceTargetPairs.length > 1 ? nls.localize('moveBulkEdit', "Move {0} files", sourceTargetPairs.length) : nls.localize('moveFileBulkEdit', "Move {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)) + progressLabel: sourceTargetPairs.length > 1 ? nls.localize({ key: 'movingBulkEdit', comment: ['Placeholder will be replaced by the number of files being moved'] }, "Moving {0} files", sourceTargetPairs.length) + : nls.localize({ key: 'movingFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file moved.'] }, "Moving {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)), + undoLabel: sourceTargetPairs.length > 1 ? nls.localize({ key: 'moveBulkEdit', comment: ['Placeholder will be replaced by the number of files being moved'] }, "Move {0} files", sourceTargetPairs.length) + : nls.localize({ key: 'moveFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file moved.'] }, "Move {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)) }; await explorerService.applyBulkEdit(resourceFileEdits, options); } else { const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target, { copy: true })); const options = { - progressLabel: sourceTargetPairs.length > 1 ? nls.localize('copyingBulkEdit', "Copying {0} files", sourceTargetPairs.length) : nls.localize('copyingFileBulkEdit', "Copying {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)), - undoLabel: sourceTargetPairs.length > 1 ? nls.localize('copyBulkEdit', "Copy {0} files", sourceTargetPairs.length) : nls.localize('copyFileBulkEdit', "Copy {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)) + progressLabel: sourceTargetPairs.length > 1 ? nls.localize({ key: 'copyingBulkEdit', comment: ['Placeholder will be replaced by the number of files being copied'] }, "Copying {0} files", sourceTargetPairs.length) + : nls.localize({ key: 'copyingFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file copied.'] }, "Copying {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)), + undoLabel: sourceTargetPairs.length > 1 ? nls.localize({ key: 'copyBulkEdit', comment: ['Placeholder will be replaced by the number of files being copied'] }, "Copy {0} files", sourceTargetPairs.length) + : nls.localize({ key: 'copyFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file copied.'] }, "Copy {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)) }; await explorerService.applyBulkEdit(resourceFileEdits, options); } diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index 8e1badc5..ef6eb4dd 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -40,8 +40,6 @@ import { coalesce } from 'vs/base/common/arrays'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { openEditorWith } from 'vs/workbench/services/editor/common/editorOpenWith'; import { isPromiseCanceledError } from 'vs/base/common/errors'; @@ -356,13 +354,11 @@ CommandsRegistry.registerCommand({ handler: async (accessor, resource: URI | object) => { const editorService = accessor.get(IEditorService); const editorGroupsService = accessor.get(IEditorGroupsService); - const configurationService = accessor.get(IConfigurationService); - const quickInputService = accessor.get(IQuickInputService); const uri = getResourceForCommand(resource, accessor.get(IListService), accessor.get(IEditorService)); if (uri) { const input = editorService.createEditorInput({ resource: uri }); - openEditorWith(input, undefined, undefined, editorGroupsService.activeGroup, editorService, configurationService, quickInputService); + openEditorWith(accessor, input, undefined, undefined, editorGroupsService.activeGroup); } } }); @@ -482,7 +478,12 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); -CommandsRegistry.registerCommand({ +KeybindingsRegistry.registerCommandAndKeybindingRule({ + when: undefined, + weight: KeybindingWeight.WorkbenchContrib, + primary: undefined, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_S }, + win: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_S) }, id: SAVE_ALL_COMMAND_ID, handler: (accessor) => { return saveDirtyEditorsOfGroups(accessor, accessor.get(IEditorGroupsService).getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE), { reason: SaveReason.EXPLICIT }); @@ -664,12 +665,10 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ if (typeof args?.viewType === 'string') { const editorGroupsService = accessor.get(IEditorGroupsService); - const configurationService = accessor.get(IConfigurationService); - const quickInputService = accessor.get(IQuickInputService); const textInput = editorService.createEditorInput({ options: { pinned: true } }); const group = editorGroupsService.activeGroup; - await openEditorWith(textInput, args.viewType, { pinned: true }, group, editorService, configurationService, quickInputService); + await openEditorWith(accessor, textInput, args.viewType, { pinned: true }, group); } else { await editorService.openEditor({ options: { pinned: true } }); // untitled are always pinned } diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 7ea77d33..f4b223bf 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -198,8 +198,8 @@ const hotExitConfiguration: IConfigurationPropertySchema = platform.isNative ? 'default': HotExitConfiguration.ON_EXIT, 'markdownEnumDescriptions': [ nls.localize('hotExit.off', 'Disable hot exit. A prompt will show when attempting to close a window with dirty files.'), - nls.localize('hotExit.onExit', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu). All windows without folders opened will be restored upon next launch. A list of workspaces with unsaved files can be accessed via `File > Open Recent > More...`'), - nls.localize('hotExit.onExitAndWindowClose', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu), and also for any window with a folder opened regardless of whether it\'s the last window. All windows without folders opened will be restored upon next launch. A list of workspaces with unsaved files can be accessed via `File > Open Recent > More...`') + nls.localize('hotExit.onExit', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu). All windows without folders opened will be restored upon next launch. A list of previously opened windows with unsaved files can be accessed via `File > Open Recent > More...`'), + nls.localize('hotExit.onExitAndWindowClose', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu), and also for any window with a folder opened regardless of whether it\'s the last window. All windows without folders opened will be restored upon next launch. A list of previously opened windows with unsaved files can be accessed via `File > Open Recent > More...`') ], 'description': nls.localize('hotExit', "Controls whether unsaved files are remembered between sessions, allowing the save prompt when exiting the editor to be skipped.", HotExitConfiguration.ON_EXIT, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) } : { @@ -386,7 +386,7 @@ configurationRegistry.registerConfiguration({ nls.localize({ key: 'everything', comment: ['This is the description of an option'] }, "Format the whole file."), nls.localize({ key: 'modification', comment: ['This is the description of an option'] }, "Format modifications (requires source control)."), ], - 'markdownDescription': nls.localize('formatOnSaveMode', "Controls if format on save formats the whole file or only modifications. Only applies when `#editor.formatOnSave#` is `true`."), + 'markdownDescription': nls.localize('formatOnSaveMode', "Controls if format on save formats the whole file or only modifications. Only applies when `#editor.formatOnSave#` is enabled."), 'scope': ConfigurationScope.LANGUAGE_OVERRIDABLE, }, } @@ -492,7 +492,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { UndoCommand.addImplementation(110, (accessor: ServicesAccessor) => { const undoRedoService = accessor.get(IUndoRedoService); const explorerService = accessor.get(IExplorerService); - if (explorerService.hasViewFocus()) { + if (explorerService.hasViewFocus() && undoRedoService.canUndo(UNDO_REDO_SOURCE)) { undoRedoService.undo(UNDO_REDO_SOURCE); return true; } @@ -503,7 +503,7 @@ UndoCommand.addImplementation(110, (accessor: ServicesAccessor) => { RedoCommand.addImplementation(110, (accessor: ServicesAccessor) => { const undoRedoService = accessor.get(IUndoRedoService); const explorerService = accessor.get(IExplorerService); - if (explorerService.hasViewFocus()) { + if (explorerService.hasViewFocus() && undoRedoService.canRedo(UNDO_REDO_SOURCE)) { undoRedoService.redo(UNDO_REDO_SOURCE); return true; } diff --git a/src/vs/workbench/contrib/files/browser/files.ts b/src/vs/workbench/contrib/files/browser/files.ts index 95e137cb..bd088034 100644 --- a/src/vs/workbench/contrib/files/browser/files.ts +++ b/src/vs/workbench/contrib/files/browser/files.ts @@ -16,6 +16,7 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { IEditableData } from 'vs/workbench/common/views'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; +import { ProgressLocation } from 'vs/platform/progress/common/progress'; export interface IExplorerService { @@ -34,7 +35,7 @@ export interface IExplorerService { refresh(): Promise; setToCopy(stats: ExplorerItem[], cut: boolean): Promise; isCut(stat: ExplorerItem): boolean; - applyBulkEdit(edit: ResourceFileEdit[], options: { undoLabel: string, progressLabel: string }): Promise; + applyBulkEdit(edit: ResourceFileEdit[], options: { undoLabel: string, progressLabel: string, confirmBeforeUndo?: boolean, progressLocation?: ProgressLocation.Explorer | ProgressLocation.Window }): Promise; /** * Selects and reveal the file element provided by the given resource if its found in the explorer. diff --git a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css index 8d645cca..29bad547 100644 --- a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css +++ b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css @@ -89,7 +89,7 @@ height: 22px; } -.monaco-workbench .explorer-viewlet .explorer-item .monaco-inputbox > .wrapper > .input { +.monaco-workbench .explorer-viewlet .explorer-item .monaco-inputbox > .ibwrapper > .input { padding: 0; height: 20px; } diff --git a/src/vs/workbench/contrib/files/browser/views/emptyView.ts b/src/vs/workbench/contrib/files/browser/views/emptyView.ts index d649859f..7e70f008 100644 --- a/src/vs/workbench/contrib/files/browser/views/emptyView.ts +++ b/src/vs/workbench/contrib/files/browser/views/emptyView.ts @@ -11,7 +11,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { ResourcesDropHandler, DragAndDropObserver } from 'vs/workbench/browser/dnd'; import { listDropBackground } from 'vs/platform/theme/common/colorRegistry'; import { ILabelService } from 'vs/platform/label/common/label'; diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index a9309b46..59ca8405 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -8,30 +8,30 @@ import { URI } from 'vs/base/common/uri'; import * as perf from 'vs/base/common/performance'; import { IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import { memoize } from 'vs/base/common/decorators'; -import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, ExplorerResourceAvailableEditorIdsContext } from 'vs/workbench/contrib/files/common/files'; -import { NewFolderAction, NewFileAction, FileCopiedContext, RefreshExplorerView, CollapseExplorerView } from 'vs/workbench/contrib/files/browser/fileActions'; +import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, ExplorerResourceAvailableEditorIdsContext, VIEW_ID, VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; +import { FileCopiedContext, NEW_FILE_COMMAND_ID, NEW_FOLDER_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileActions'; import * as DOM from 'vs/base/browser/dom'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { ExplorerDecorationsProvider } from 'vs/workbench/contrib/files/browser/views/explorerDecorationsProvider'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKey, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { IDecorationsService } from 'vs/workbench/services/decorations/browser/decorations'; import { WorkbenchCompressibleAsyncDataTree } from 'vs/platform/list/browser/listService'; import { DelayedDragHandler } from 'vs/base/browser/dnd'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IViewPaneOptions, ViewPane, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { ILabelService } from 'vs/platform/label/common/label'; import { ExplorerDelegate, ExplorerDataSource, FilesRenderer, ICompressedNavigationController, FilesFilter, FileSorter, FileDragAndDrop, ExplorerCompressionDelegate, isCompressedFolderName } from 'vs/workbench/contrib/files/browser/views/explorerViewer'; import { IThemeService, IFileIconTheme } from 'vs/platform/theme/common/themeService'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ITreeContextMenuEvent, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; -import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId, IMenu, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; @@ -52,6 +52,9 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; +import { Codicon } from 'vs/base/common/codicons'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; interface IExplorerViewColors extends IColorMapping { listDropBackground?: ColorValue | undefined; @@ -75,6 +78,16 @@ function hasExpandedRootChild(tree: WorkbenchCompressibleAsyncDataTree { + if (stat instanceof NewExplorerItem) { + return `new:${stat.resource}`; + } + + return stat.resource; + } +}; + export function getContext(focus: ExplorerItem[], selection: ExplorerItem[], respectMultiSelection: boolean, compressedNavigationControllerProvider: { getCompressedNavigationController(stat: ExplorerItem): ICompressedNavigationController | undefined }): ExplorerItem[] { @@ -146,7 +159,6 @@ export class ExplorerView extends ViewPane { private shouldRefresh = true; private dragHandler!: DelayedDragHandler; private autoReveal: boolean | 'focusNoScroll' = false; - private actions: IAction[] | undefined; private decorationsProvider: ExplorerDecorationsProvider | undefined; constructor( @@ -284,19 +296,6 @@ export class ExplorerView extends ViewPane { })); } - getActions(): IAction[] { - if (!this.actions) { - this.actions = [ - this.instantiationService.createInstance(NewFileAction), - this.instantiationService.createInstance(NewFolderAction), - this.instantiationService.createInstance(RefreshExplorerView, RefreshExplorerView.ID, RefreshExplorerView.LABEL), - this.instantiationService.createInstance(CollapseExplorerView, CollapseExplorerView.ID, CollapseExplorerView.LABEL) - ]; - this.actions.forEach(a => this._register(a)); - } - return this.actions; - } - focus(): void { this.tree.domFocus(); @@ -381,15 +380,7 @@ export class ExplorerView extends ViewPane { this.instantiationService.createInstance(ExplorerDataSource), { compressionEnabled: isCompressionEnabled(), accessibilityProvider: this.renderer, - identityProvider: { - getId: (stat: ExplorerItem) => { - if (stat instanceof NewExplorerItem) { - return `new:${stat.resource}`; - } - - return stat.resource; - } - }, + identityProvider, keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (stat: ExplorerItem) => { if (this.explorerService.isEditable(stat)) { @@ -593,7 +584,9 @@ export class ExplorerView extends ViewPane { } const toRefresh = item || this.tree.getInput(); - return this.tree.updateChildren(toRefresh, recursive); + return this.tree.updateChildren(toRefresh, recursive, false, { + diffIdentityProvider: identityProvider + }); } focusNeighbourIfItemFocused(item: ExplorerItem): void { @@ -635,7 +628,7 @@ export class ExplorerView extends ViewPane { const initialInputSetup = !this.tree.getInput(); if (initialInputSetup) { - perf.mark('willResolveExplorer'); + perf.mark('code/willResolveExplorer'); } const roots = this.explorerService.roots; let input: ExplorerItem | ExplorerItem[] = roots[0]; @@ -675,7 +668,7 @@ export class ExplorerView extends ViewPane { } } if (initialInputSetup) { - perf.mark('didResolveExplorer'); + perf.mark('code/didResolveExplorer'); } }); @@ -856,3 +849,93 @@ function createFileIconThemableTreeContainerScope(container: HTMLElement, themeS onDidChangeFileIconTheme(themeService.getFileIconTheme()); return themeService.onDidFileIconThemeChange(onDidChangeFileIconTheme); } + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.files.action.createFileFromExplorer', + title: nls.localize('createNewFile', "New File"), + f1: false, + icon: Codicon.newFile, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', VIEW_ID), + order: 10 + } + }); + } + + run(accessor: ServicesAccessor): void { + const commandService = accessor.get(ICommandService); + commandService.executeCommand(NEW_FILE_COMMAND_ID); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.files.action.createFolderFromExplorer', + title: nls.localize('createNewFolder', "New Folder"), + f1: false, + icon: Codicon.newFolder, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', VIEW_ID), + order: 20 + } + }); + } + + run(accessor: ServicesAccessor): void { + const commandService = accessor.get(ICommandService); + commandService.executeCommand(NEW_FOLDER_COMMAND_ID); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.files.action.refreshFilesExplorer', + title: nls.localize('refreshExplorer', "Refresh Explorer"), + f1: true, + icon: Codicon.refresh, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', VIEW_ID), + order: 30 + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const viewletService = accessor.get(IViewletService); + const explorerService = accessor.get(IExplorerService); + await viewletService.openViewlet(VIEWLET_ID); + await explorerService.refresh(); + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'workbench.files.action.collapseExplorerFolders', + title: nls.localize('collapseExplorerFolders', "Collapse Folders in Explorer"), + viewId: VIEW_ID, + f1: true, + icon: Codicon.collapseAll, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', VIEW_ID), + order: 40 + } + }); + } + + runInView(_accessor: ServicesAccessor, view: ExplorerView): void { + view.collapseAll(); + } +}); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 9e8330b3..6bf781d2 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -451,7 +451,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer { if (e.equals(KeyCode.Enter)) { - if (inputBox.validate()) { + if (!inputBox.validate()) { done(true, true); } } else if (e.equals(KeyCode.Escape)) { @@ -626,7 +626,7 @@ export class FilesFilter implements ITreeFilter { stat.isExcluded = true; return false; } - if (this.explorerService.getEditableData(stat) || stat.isRoot) { + if (this.explorerService.getEditableData(stat)) { return true; // always visible } diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 3535108c..db0b429c 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -9,16 +9,15 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { IAction, ActionRunner, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import * as dom from 'vs/base/browser/dom'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorGroupsService, IEditorGroup, GroupChangeKind, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IEditorGroupsService, IEditorGroup, GroupChangeKind, GroupsOrder, GroupOrientation } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IEditorInput, Verbosity, EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; -import { SaveAllAction, SaveAllInGroupAction, CloseGroupAction } from 'vs/workbench/contrib/files/browser/fileActions'; +import { SaveAllInGroupAction, CloseGroupAction } from 'vs/workbench/contrib/files/browser/fileActions'; import { OpenEditorsFocusedContext, ExplorerFocusedContext, IFilesConfiguration, OpenEditor } from 'vs/workbench/contrib/files/common/files'; import { CloseAllEditorsAction, CloseEditorAction, UnpinEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; -import { ToggleEditorLayoutAction } from 'vs/workbench/browser/actions/layoutActions'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKey, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { badgeBackground, badgeForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; @@ -30,11 +29,11 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; -import { OpenEditorsDirtyEditorContext, OpenEditorsGroupContext, OpenEditorsReadonlyEditorContext } from 'vs/workbench/contrib/files/browser/fileCommands'; +import { IMenuService, MenuId, IMenu, Action2, registerAction2, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { OpenEditorsDirtyEditorContext, OpenEditorsGroupContext, OpenEditorsReadonlyEditorContext, SAVE_ALL_LABEL, SAVE_ALL_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { ResourcesDropHandler, fillResourceDataTransfers, CodeDataTransfers, containsDragType } from 'vs/workbench/browser/dnd'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd'; import { memoize } from 'vs/base/common/decorators'; @@ -49,6 +48,10 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { compareFileNamesDefault } from 'vs/base/common/comparers'; +import { Codicon } from 'vs/base/common/codicons'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { ICommandService } from 'vs/platform/commands/common/commands'; const $ = dom.$; @@ -92,14 +95,26 @@ export class OpenEditorsView extends ViewPane { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.structuralRefreshDelay = 0; + let labelChangeListeners: IDisposable[] = []; this.listRefreshScheduler = new RunOnceScheduler(() => { + labelChangeListeners = dispose(labelChangeListeners); const previousLength = this.list.length; - this.list.splice(0, this.list.length, this.getElements()); + const elements = this.getElements(); + this.list.splice(0, this.list.length, elements); this.focusActiveEditor(); if (previousLength !== this.list.length) { this.updateSize(); } this.needsRefresh = false; + + if (this.sortOrder === 'alphabetical') { + // We need to resort the list if the editor label changed + elements.forEach(e => { + if (e instanceof OpenEditor) { + labelChangeListeners.push(e.editor.onDidChangeLabel(() => this.listRefreshScheduler.schedule())); + } + }); + } }, this.structuralRefreshDelay); this.sortOrder = configurationService.getValue('explorer.openEditors.sortOrder'); @@ -151,6 +166,7 @@ export class OpenEditorsView extends ViewPane { case GroupChangeKind.EDITOR_STICKY: case GroupChangeKind.EDITOR_PIN: { this.list.splice(index, 1, [new OpenEditor(e.editor!, group)]); + this.focusActiveEditor(); break; } case GroupChangeKind.EDITOR_OPEN: @@ -294,14 +310,6 @@ export class OpenEditorsView extends ViewPane { })); } - getActions(): IAction[] { - return [ - this.instantiationService.createInstance(ToggleEditorLayoutAction, ToggleEditorLayoutAction.ID, ToggleEditorLayoutAction.LABEL), - this.instantiationService.createInstance(SaveAllAction, SaveAllAction.ID, SaveAllAction.LABEL), - this.instantiationService.createInstance(CloseAllEditorsAction, CloseAllEditorsAction.ID, CloseAllEditorsAction.LABEL) - ]; - } - focus(): void { super.focus(); this.list.domFocus(); @@ -705,3 +713,86 @@ class OpenEditorsAccessibilityProvider implements IListAccessibilityProvider { + const editorGroupService = accessor.get(IEditorGroupsService); + const newOrientation = (editorGroupService.orientation === GroupOrientation.VERTICAL) ? GroupOrientation.HORIZONTAL : GroupOrientation.VERTICAL; + editorGroupService.setGroupOrientation(newOrientation); + } +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: 'z_flip', + command: { + id: toggleEditorGroupLayoutId, + title: nls.localize({ key: 'miToggleEditorLayout', comment: ['&& denotes a mnemonic'] }, "Flip &&Layout") + }, + order: 1 +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.files.saveAll', + title: SAVE_ALL_LABEL, + f1: true, + icon: Codicon.saveAll, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', OpenEditorsView.ID), + order: 20 + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const commandService = accessor.get(ICommandService); + await commandService.executeCommand(SAVE_ALL_COMMAND_ID); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'openEditors.closeAll', + title: CloseAllEditorsAction.LABEL, + f1: false, + icon: Codicon.closeAll, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', OpenEditorsView.ID), + order: 30 + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const instantiationService = accessor.get(IInstantiationService); + const closeAll = instantiationService.createInstance(CloseAllEditorsAction, CloseAllEditorsAction.ID, CloseAllEditorsAction.LABEL); + await closeAll.run(); + } +}); diff --git a/src/vs/workbench/contrib/files/common/workspaceWatcher.ts b/src/vs/workbench/contrib/files/common/workspaceWatcher.ts index 04bfd96d..1534bdc8 100644 --- a/src/vs/workbench/contrib/files/common/workspaceWatcher.ts +++ b/src/vs/workbench/contrib/files/common/workspaceWatcher.ts @@ -89,7 +89,7 @@ export class WorkspaceWatcher extends Disposable { if (msg.indexOf('ENOSPC') >= 0) { this.notificationService.prompt( Severity.Warning, - localize('enospcError', "Unable to watch for file changes in this large workspace. Please follow the instructions link to resolve this issue."), + localize('enospcError', "Unable to watch for file changes in this large workspace folder. Please follow the instructions link to resolve this issue."), [{ label: localize('learnMore', "Instructions"), run: () => this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?linkid=867693')) diff --git a/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts b/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts index d310e987..145efa74 100644 --- a/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; +import { localize } from 'vs/nls'; import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { EditorOptions } from 'vs/workbench/common/editor'; import { FileOperationError, FileOperationResult, IFileService, MIN_MAX_MEMORY_SIZE_MB, FALLBACK_MAX_MEMORY_SIZE_MB } from 'vs/platform/files/common/files'; import { createErrorWithActions } from 'vs/base/common/errorsWithActions'; -import { Action } from 'vs/base/common/actions'; +import { toAction } from 'vs/base/common/actions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -56,18 +56,22 @@ export class NativeTextFileEditor extends TextFileEditor { if ((error).fileOperationResult === FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT) { const memoryLimit = Math.max(MIN_MAX_MEMORY_SIZE_MB, +this.textResourceConfigurationService.getValue(undefined, 'files.maxMemoryForLargeFilesMB') || FALLBACK_MAX_MEMORY_SIZE_MB); - throw createErrorWithActions(nls.localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow it to use more memory"), { + throw createErrorWithActions(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow it to use more memory"), { actions: [ - new Action('workbench.window.action.relaunchWithIncreasedMemoryLimit', nls.localize('relaunchWithIncreasedMemoryLimit', "Restart with {0} MB", memoryLimit), undefined, true, () => { - return this.nativeHostService.relaunch({ - addArgs: [ - `--max-memory=${memoryLimit}` - ] - }); + toAction({ + id: 'workbench.window.action.relaunchWithIncreasedMemoryLimit', label: localize('relaunchWithIncreasedMemoryLimit', "Restart with {0} MB", memoryLimit), run: () => { + return this.nativeHostService.relaunch({ + addArgs: [ + `--max-memory=${memoryLimit}` + ] + }); + } + }), + toAction({ + id: 'workbench.window.action.configureMemoryLimit', label: localize('configureMemoryLimit', 'Configure Memory Limit'), run: () => { + return this.preferencesService.openGlobalSettings(undefined, { query: 'files.maxMemoryForLargeFilesMB' }); + } }), - new Action('workbench.window.action.configureMemoryLimit', nls.localize('configureMemoryLimit', 'Configure Memory Limit'), undefined, true, () => { - return this.preferencesService.openGlobalSettings(undefined, { query: 'files.maxMemoryForLargeFilesMB' }); - }) ] }); } diff --git a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts index 644cdf74..821be988 100644 --- a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts @@ -119,8 +119,6 @@ suite('EditorAutoSave', () => { }); function awaitModelSaved(model: ITextFileEditorModel): Promise { - return new Promise(c => { - Event.once(model.onDidChangeDirty)(c); - }); + return Event.toPromise(Event.once(model.onDidChangeDirty)); } }); diff --git a/src/vs/workbench/contrib/files/test/browser/fileActions.test.ts b/src/vs/workbench/contrib/files/test/browser/fileActions.test.ts index bf0642cd..bfa18a3b 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileActions.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileActions.test.ts @@ -258,7 +258,7 @@ suite('Files - Increment file name smart', () => { assert.strictEqual(result, '2-test.js'); }); - test('Increment file name with prefix version with `-` as separator', function () { + test('Increment file name with prefix version with `_` as separator', function () { const name = '1_test.js'; const result = incrementFileName(name, false, 'smart'); assert.strictEqual(result, '2_test.js'); @@ -270,6 +270,36 @@ suite('Files - Increment file name smart', () => { assert.strictEqual(result, '9007199254740992.test.1.js'); }); + test('Increment file name with just version and no extension', function () { + const name = '001004'; + const result = incrementFileName(name, false, 'smart'); + assert.strictEqual(result, '001005'); + }); + + test('Increment file name with just version and no extension, too big number', function () { + const name = '9007199254740992'; + const result = incrementFileName(name, false, 'smart'); + assert.strictEqual(result, '9007199254740992.1'); + }); + + test('Increment file name with no extension and no version', function () { + const name = 'file'; + const result = incrementFileName(name, false, 'smart'); + assert.strictEqual(result, 'file1'); + }); + + test('Increment file name with no extension', function () { + const name = 'file1'; + const result = incrementFileName(name, false, 'smart'); + assert.strictEqual(result, 'file2'); + }); + + test('Increment file name with no extension, too big number', function () { + const name = 'file9007199254740992'; + const result = incrementFileName(name, false, 'smart'); + assert.strictEqual(result, 'file9007199254740992.1'); + }); + test('Increment folder name with prefix version', function () { const name = '1.test'; const result = incrementFileName(name, true, 'smart'); diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts index 0df2ed5d..bb2a00dd 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -102,28 +102,28 @@ suite('Files - FileEditorInput', () => { const preferredResource = toResource.call(this, '/foo/bar/UPDATEFILE.js'); const inputWithoutPreferredResource = createFileInput(resource); - assert.equal(inputWithoutPreferredResource.resource.toString(), resource.toString()); - assert.equal(inputWithoutPreferredResource.preferredResource.toString(), resource.toString()); + assert.strictEqual(inputWithoutPreferredResource.resource.toString(), resource.toString()); + assert.strictEqual(inputWithoutPreferredResource.preferredResource.toString(), resource.toString()); const inputWithPreferredResource = createFileInput(resource, preferredResource); - assert.equal(inputWithPreferredResource.resource.toString(), resource.toString()); - assert.equal(inputWithPreferredResource.preferredResource.toString(), preferredResource.toString()); + assert.strictEqual(inputWithPreferredResource.resource.toString(), resource.toString()); + assert.strictEqual(inputWithPreferredResource.preferredResource.toString(), preferredResource.toString()); let didChangeLabel = false; const listener = inputWithPreferredResource.onDidChangeLabel(e => { didChangeLabel = true; }); - assert.equal(inputWithPreferredResource.getName(), 'UPDATEFILE.js'); + assert.strictEqual(inputWithPreferredResource.getName(), 'UPDATEFILE.js'); const otherPreferredResource = toResource.call(this, '/FOO/BAR/updateFILE.js'); inputWithPreferredResource.setPreferredResource(otherPreferredResource); - assert.equal(inputWithPreferredResource.resource.toString(), resource.toString()); - assert.equal(inputWithPreferredResource.preferredResource.toString(), otherPreferredResource.toString()); - assert.equal(inputWithPreferredResource.getName(), 'updateFILE.js'); - assert.equal(didChangeLabel, true); + assert.strictEqual(inputWithPreferredResource.resource.toString(), resource.toString()); + assert.strictEqual(inputWithPreferredResource.preferredResource.toString(), otherPreferredResource.toString()); + assert.strictEqual(inputWithPreferredResource.getName(), 'updateFILE.js'); + assert.strictEqual(didChangeLabel, true); listener.dispose(); }); @@ -135,20 +135,20 @@ suite('Files - FileEditorInput', () => { }); const input = createFileInput(toResource.call(this, '/foo/bar/file.js'), undefined, mode); - assert.equal(input.getPreferredMode(), mode); + assert.strictEqual(input.getPreferredMode(), mode); const model = await input.resolve() as TextFileEditorModel; - assert.equal(model.textEditorModel!.getModeId(), mode); + assert.strictEqual(model.textEditorModel!.getModeId(), mode); input.setMode('text'); - assert.equal(input.getPreferredMode(), 'text'); - assert.equal(model.textEditorModel!.getModeId(), PLAINTEXT_MODE_ID); + assert.strictEqual(input.getPreferredMode(), 'text'); + assert.strictEqual(model.textEditorModel!.getModeId(), PLAINTEXT_MODE_ID); const input2 = createFileInput(toResource.call(this, '/foo/bar/file.js')); input2.setPreferredMode(mode); const model2 = await input2.resolve() as TextFileEditorModel; - assert.equal(model2.textEditorModel!.getModeId(), mode); + assert.strictEqual(model2.textEditorModel!.getModeId(), mode); }); test('matches', function () { @@ -169,10 +169,10 @@ suite('Files - FileEditorInput', () => { const input = createFileInput(toResource.call(this, '/foo/bar/updatefile.js')); input.setEncoding('utf16', EncodingMode.Encode); - assert.equal(input.getEncoding(), 'utf16'); + assert.strictEqual(input.getEncoding(), 'utf16'); const resolved = await input.resolve() as TextFileEditorModel; - assert.equal(input.getEncoding(), resolved.getEncoding()); + assert.strictEqual(input.getEncoding(), resolved.getEncoding()); resolved.dispose(); }); @@ -237,7 +237,7 @@ suite('Files - FileEditorInput', () => { const model = await accessor.textFileService.files.resolve(input.resource); model.textEditorModel?.setValue('hello world'); - assert.equal(listenerCount, 1); + assert.strictEqual(listenerCount, 1); assert.ok(input.isDirty()); input.dispose(); @@ -269,7 +269,7 @@ suite('Files - FileEditorInput', () => { assert.fail('File Editor Input Factory missing'); } - assert.equal(factory.canSerialize(input), true); + assert.strictEqual(factory.canSerialize(input), true); const inputSerialized = factory.serialize(input); if (!inputSerialized) { @@ -277,7 +277,7 @@ suite('Files - FileEditorInput', () => { } const inputDeserialized = factory.deserialize(instantiationService, inputSerialized); - assert.equal(input.matches(inputDeserialized), true); + assert.strictEqual(input.matches(inputDeserialized), true); const preferredResource = toResource.call(this, '/foo/bar/UPDATEfile.js'); const inputWithPreferredResource = createFileInput(toResource.call(this, '/foo/bar/updatefile.js'), preferredResource); @@ -288,8 +288,8 @@ suite('Files - FileEditorInput', () => { } const inputWithPreferredResourceDeserialized = factory.deserialize(instantiationService, inputWithPreferredResourceSerialized) as FileEditorInput; - assert.equal(inputWithPreferredResource.resource.toString(), inputWithPreferredResourceDeserialized.resource.toString()); - assert.equal(inputWithPreferredResource.preferredResource.toString(), inputWithPreferredResourceDeserialized.preferredResource.toString()); + assert.strictEqual(inputWithPreferredResource.resource.toString(), inputWithPreferredResourceDeserialized.resource.toString()); + assert.strictEqual(inputWithPreferredResource.preferredResource.toString(), inputWithPreferredResourceDeserialized.preferredResource.toString()); }); test('preferred name/description', async function () { @@ -302,16 +302,16 @@ suite('Files - FileEditorInput', () => { didChangeLabelCounter++; }); - assert.equal(customFileInput.getName(), 'My Name'); - assert.equal(customFileInput.getDescription(), 'My Description'); + assert.strictEqual(customFileInput.getName(), 'My Name'); + assert.strictEqual(customFileInput.getDescription(), 'My Description'); customFileInput.setPreferredName('My Name 2'); customFileInput.setPreferredDescription('My Description 2'); - assert.equal(customFileInput.getName(), 'My Name 2'); - assert.equal(customFileInput.getDescription(), 'My Description 2'); + assert.strictEqual(customFileInput.getName(), 'My Name 2'); + assert.strictEqual(customFileInput.getDescription(), 'My Description 2'); - assert.equal(didChangeLabelCounter, 2); + assert.strictEqual(didChangeLabelCounter, 2); customFileInput.dispose(); @@ -332,7 +332,7 @@ suite('Files - FileEditorInput', () => { assert.notEqual(fileInput.getName(), 'My Name 2'); assert.notEqual(fileInput.getDescription(), 'My Description 2'); - assert.equal(didChangeLabelCounter, 0); + assert.strictEqual(didChangeLabelCounter, 0); fileInput.dispose(); }); diff --git a/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts b/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts index 70c2fa9e..00b36384 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts @@ -27,8 +27,8 @@ suite('Files - FileOnDiskContentProvider', () => { const content = await provider.provideTextContent(uri.with({ scheme: 'conflictResolution', query: JSON.stringify({ scheme: uri.scheme }) })); assert.ok(content); - assert.equal(snapshotToString(content!.createSnapshot()), 'Hello Html'); - assert.equal(accessor.fileService.getLastReadFileUri().scheme, uri.scheme); - assert.equal(accessor.fileService.getLastReadFileUri().path, uri.path); + assert.strictEqual(snapshotToString(content!.createSnapshot()), 'Hello Html'); + assert.strictEqual(accessor.fileService.getLastReadFileUri().scheme, uri.scheme); + assert.strictEqual(accessor.fileService.getLastReadFileUri().path, uri.path); }); }); diff --git a/src/vs/workbench/contrib/files/test/browser/textFileEditor.test.ts b/src/vs/workbench/contrib/files/test/browser/textFileEditor.test.ts index 9b346315..33342c8f 100644 --- a/src/vs/workbench/contrib/files/test/browser/textFileEditor.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/textFileEditor.test.ts @@ -27,6 +27,7 @@ import { IFilesConfigurationService } from 'vs/workbench/services/filesConfigura import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { raceTimeout } from 'vs/base/common/async'; suite('Files - TextFileEditor', () => { @@ -73,7 +74,7 @@ suite('Files - TextFileEditor', () => { const accessor = instantiationService.createInstance(TestServiceAccessor); - await part.whenRestored; + await raceTimeout(part.whenRestored, 2000, () => assert.fail('textFileEditor.test.ts: Unexpected long time to wait for part to restore (#112649)')); return [part, accessor, instantiationService, editorService]; } @@ -86,7 +87,7 @@ suite('Files - TextFileEditor', () => { return viewStateTest(this, false); }); - async function viewStateTest(context: Mocha.ITestCallbackContext, restoreViewState: boolean): Promise { + async function viewStateTest(context: Mocha.Context, restoreViewState: boolean): Promise { const [part, accessor] = await createPart(restoreViewState); let editor = await accessor.editorService.openEditor(accessor.editorService.createEditorInput({ resource: toResource.call(context, '/path/index.txt'), forceFile: true })); diff --git a/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts index a869de37..2ce57b49 100644 --- a/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts @@ -94,7 +94,7 @@ suite('Files - TextFileEditorTracker', () => { const model = await accessor.textFileService.files.resolve(resource) as IResolvedTextFileEditorModel; model.textEditorModel.setValue('Super Good'); - assert.equal(snapshotToString(model.createSnapshot()!), 'Super Good'); + assert.strictEqual(snapshotToString(model.createSnapshot()!), 'Super Good'); await model.save(); @@ -103,7 +103,7 @@ suite('Files - TextFileEditorTracker', () => { await timeout(0); // due to event updating model async - assert.equal(snapshotToString(model.createSnapshot()!), 'Hello Html'); + assert.strictEqual(snapshotToString(model.createSnapshot()!), 'Hello Html'); tracker.dispose(); (accessor.textFileService.files).dispose(); @@ -162,9 +162,7 @@ suite('Files - TextFileEditorTracker', () => { }); function awaitEditorOpening(editorService: IEditorService): Promise { - return new Promise(c => { - Event.once(editorService.onDidActiveEditorChange)(c); - }); + return Event.toPromise(Event.once(editorService.onDidActiveEditorChange)); } test('non-dirty files reload on window focus', async function () { diff --git a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts index 05c0d93f..14a4c09a 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts @@ -30,6 +30,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { editorConfigurationBaseNode } from 'vs/editor/common/config/commonEditorConfig'; import { mergeSort } from 'vs/base/common/arrays'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; type FormattingEditProvider = DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider; @@ -45,6 +46,7 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { @IWorkbenchExtensionEnablementService private readonly _extensionEnablementService: IWorkbenchExtensionEnablementService, @IConfigurationService private readonly _configService: IConfigurationService, @INotificationService private readonly _notificationService: INotificationService, + @IDialogService private readonly _dialogService: IDialogService, @IQuickInputService private readonly _quickInputService: IQuickInputService, @IModeService private readonly _modeService: IModeService, @ILabelService private readonly _labelService: ILabelService, @@ -119,25 +121,32 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { } const langName = this._modeService.getLanguageName(document.getModeId()) || document.getModeId(); - const silent = mode === FormattingMode.Silent; const message = !defaultFormatterId ? nls.localize('config.needed', "There are multiple formatters for '{0}' files. Select a default formatter to continue.", DefaultFormatter._maybeQuotes(langName)) : nls.localize('config.bad', "Extension '{0}' is configured as formatter but not available. Select a different default formatter to continue.", defaultFormatterId); - return new Promise((resolve, reject) => { + if (mode !== FormattingMode.Silent) { + // running from a user action -> show modal dialog so that users configure + // a default formatter + const result = await this._dialogService.confirm({ + message, + primaryButton: nls.localize('do.config', "Configure..."), + secondaryButton: nls.localize('cancel', "Cancel") + }); + if (result.confirmed) { + return this._pickAndPersistDefaultFormatter(formatter, document); + } + + } else { + // no user action -> show a silent notification and proceed this._notificationService.prompt( Severity.Info, message, - [{ label: nls.localize('do.config', "Configure..."), run: () => this._pickAndPersistDefaultFormatter(formatter, document).then(resolve, reject) }], - { silent, onCancel: () => resolve(undefined) } + [{ label: nls.localize('do.config', "Configure..."), run: () => this._pickAndPersistDefaultFormatter(formatter, document) }], + { silent: true } ); - - if (silent) { - // don't wait when formatting happens without interaction - // but pick some formatter... - resolve(undefined); - } - }); + } + return undefined; } private async _pickAndPersistDefaultFormatter(formatter: T[], document: ITextModel): Promise { diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts index f7beaf0f..94beb164 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts @@ -6,7 +6,7 @@ import { IssueReporterStyles, IssueReporterData, ProcessExplorerData, IssueReporterExtensionData } from 'vs/platform/issue/common/issue'; import { IIssueService } from 'vs/platform/issue/electron-sandbox/issue'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { textLinkForeground, inputBackground, inputBorder, inputForeground, buttonBackground, buttonHoverBackground, buttonForeground, inputValidationErrorBorder, foreground, inputActiveOptionBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, editorBackground, editorForeground, listHoverBackground, listHoverForeground, listHighlightForeground, textLinkActiveForeground, inputValidationErrorBackground, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { textLinkForeground, inputBackground, inputBorder, inputForeground, buttonBackground, buttonHoverBackground, buttonForeground, inputValidationErrorBorder, foreground, inputActiveOptionBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, editorBackground, editorForeground, listHoverBackground, listHoverForeground, textLinkActiveForeground, inputValidationErrorBackground, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -17,6 +17,7 @@ import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; +import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; export class WorkbenchIssueService implements IWorkbenchIssueService { declare readonly _serviceBrand: undefined; @@ -28,7 +29,8 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, @IProductService private readonly productService: IProductService, - @ITASExperimentService private readonly experimentService: ITASExperimentService + @ITASExperimentService private readonly experimentService: ITASExperimentService, + @IAuthenticationService private readonly authenticationService: IAuthenticationService ) { } async openReporter(dataOverrides: Partial = {}): Promise { @@ -52,12 +54,15 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { }; }); const experiments = await this.experimentService.getCurrentExperiments(); + const githubSessions = await this.authenticationService.getSessions('github'); + const potentialSessions = githubSessions.filter(session => session.scopes.includes('repo')); const theme = this.themeService.getColorTheme(); const issueReporterData: IssueReporterData = Object.assign({ styles: getIssueReporterStyles(theme), zoomLevel: getZoomLevel(), enabledExtensions: extensionData, - experiments: experiments?.join('\n') + experiments: experiments?.join('\n'), + githubAccessToken: potentialSessions[0]?.accessToken }, dataOverrides); return this.issueService.openReporter(issueReporterData); } @@ -71,8 +76,7 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { backgroundColor: getColor(theme, editorBackground), color: getColor(theme, editorForeground), hoverBackground: getColor(theme, listHoverBackground), - hoverForeground: getColor(theme, listHoverForeground), - highlightForeground: getColor(theme, listHighlightForeground), + hoverForeground: getColor(theme, listHoverForeground) }, platform: process.platform, applicationName: this.productService.applicationName diff --git a/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts b/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts index f8b4c158..a2dd3271 100644 --- a/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts +++ b/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts @@ -60,7 +60,7 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo updateAndRestart ? localize('updateLocale', "Would you like to change VS Code's UI language to {0} and restart?", e.local.manifest.contributes.localizations[0].languageName || e.local.manifest.contributes.localizations[0].languageId) : localize('activateLanguagePack', "In order to use VS Code in {0}, VS Code needs to restart.", e.local.manifest.contributes.localizations[0].languageName || e.local.manifest.contributes.localizations[0].languageId), [{ - label: updateAndRestart ? localize('yes', "Yes") : localize('restart now', "Restart Now"), + label: updateAndRestart ? localize('changeAndRestart', "Change Language and Restart") : localize('restart', "Restart"), run: () => { const updatePromise = updateAndRestart ? this.jsonEditingService.write(this.environmentService.argvResource, [{ path: ['locale'], value: locale }], true) : Promise.resolve(undefined); updatePromise.then(() => this.hostService.restart(), e => this.notificationService.error(e)); diff --git a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts index 208fea76..d65e9102 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts @@ -4,35 +4,33 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/workbench/contrib/markers/browser/markersFileDecorations'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { IWorkbenchActionRegistry, Extensions as ActionExtensions, CATEGORIES } from 'vs/workbench/common/actions'; +import { CATEGORIES } from 'vs/workbench/common/actions'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; 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 } from 'vs/workbench/contrib/markers/browser/markersView'; -import { MenuId, MenuRegistry, SyncActionDescriptor, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { MenuId, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ShowProblemsPanelAction } from 'vs/workbench/contrib/markers/browser/markersViewActions'; import Constants from 'vs/workbench/contrib/markers/browser/constants'; import Messages from 'vs/workbench/contrib/markers/browser/messages'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { ActivityUpdater } from 'vs/workbench/contrib/markers/browser/markers'; +import { ActivityUpdater, IMarkersView } from 'vs/workbench/contrib/markers/browser/markers'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { Disposable } from 'vs/base/common/lifecycle'; import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar'; import { IMarkerService, MarkerStatistics } from 'vs/platform/markers/common/markers'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewsRegistry, IViewsService, getVisbileViewContextKey, FocusedViewContext, IViewDescriptorService } from 'vs/workbench/common/views'; +import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewsRegistry, IViewsService, getVisbileViewContextKey, FocusedViewContext } 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'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ToggleViewAction } from 'vs/workbench/browser/actions/layoutActions'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.MARKER_OPEN_ACTION_ID, @@ -107,24 +105,10 @@ Registry.as(Extensions.Configuration).registerConfigurat } }); -class ToggleMarkersPanelAction extends ToggleViewAction { - - public static readonly ID = 'workbench.actions.view.problems'; - public static readonly LABEL = Messages.MARKERS_PANEL_TOGGLE_LABEL; - - constructor(id: string, label: string, - @IViewsService viewsService: IViewsService, - @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IContextKeyService contextKeyService: IContextKeyService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService - ) { - super(id, label, Constants.MARKERS_VIEW_ID, viewsService, viewDescriptorService, contextKeyService, layoutService); - } -} - const markersViewIcon = registerIcon('markers-view-icon', Codicon.warning, localize('markersViewIcon', 'View icon of the markers view.')); // markers view container +const TOGGLE_MARKERS_VIEW_ACTION_ID = 'workbench.actions.view.problems'; const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: Constants.MARKERS_CONTAINER_ID, name: Messages.MARKERS_PANEL_TITLE_PROBLEMS, @@ -134,7 +118,8 @@ const VIEW_CONTAINER: ViewContainer = Registry.as(ViewC ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [Constants.MARKERS_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), storageId: Constants.MARKERS_VIEW_STORAGE_ID, focusCommand: { - id: ToggleMarkersPanelAction.ID, keybindings: { + id: TOGGLE_MARKERS_VIEW_ACTION_ID, + keybindings: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_M } } @@ -154,12 +139,47 @@ const workbenchRegistry = Registry.as(Workbench workbenchRegistry.registerWorkbenchContribution(ActivityUpdater, LifecyclePhase.Restored); // actions -const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleMarkersPanelAction, { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_M -}), 'View: Toggle Problems (Errors, Warnings, Infos)', CATEGORIES.View.value); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ShowProblemsPanelAction), 'View: Focus Problems (Errors, Warnings, Infos)', CATEGORIES.View.value); registerAction2(class extends Action2 { + constructor() { + super({ + id: TOGGLE_MARKERS_VIEW_ACTION_ID, + title: { value: Messages.MARKERS_PANEL_TOGGLE_LABEL, original: 'Toggle Problems (Errors, Warnings, Infos)' }, + category: CATEGORIES.View.value, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_M + } + }); + } + async run(accessor: ServicesAccessor): Promise { + return accessor.get(IInstantiationService).createInstance(ToggleViewAction, TOGGLE_MARKERS_VIEW_ACTION_ID, 'Toggle Problems (Errors, Warnings, Infos)', Constants.MARKERS_VIEW_ID).run(); + } +}); +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '4_panels', + command: { + id: TOGGLE_MARKERS_VIEW_ACTION_ID, + title: localize({ key: 'miMarker', comment: ['&& denotes a mnemonic'] }, "&&Problems") + }, + order: 4 +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.problems.focus', + title: { value: Messages.MARKERS_PANEL_SHOW_LABEL, original: 'Focus Problems (Errors, Warnings, Infos)' }, + category: CATEGORIES.View.value, + f1: true, + }); + } + async run(accessor: ServicesAccessor): Promise { + accessor.get(IViewsService).openView(Constants.MARKERS_VIEW_ID, true); + } +}); + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.MARKER_COPY_ACTION_ID, @@ -174,13 +194,19 @@ registerAction2(class extends Action2 { primary: KeyMod.CtrlCmd | KeyCode.KEY_C, when: Constants.MarkerFocusContextKey }, + viewId: Constants.MARKERS_VIEW_ID }); } - async run(accessor: ServicesAccessor) { - await copyMarker(accessor.get(IViewsService), accessor.get(IClipboardService)); + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + const clipboardService = serviceAccessor.get(IClipboardService); + const element = markersView.getFocusElement(); + if (element instanceof Marker) { + await clipboardService.writeText(`${element}`); + } } }); -registerAction2(class extends Action2 { + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.MARKER_COPY_MESSAGE_ACTION_ID, @@ -190,13 +216,19 @@ registerAction2(class extends Action2 { when: Constants.MarkerFocusContextKey, group: 'navigation' }, + viewId: Constants.MARKERS_VIEW_ID }); } - async run(accessor: ServicesAccessor) { - await copyMessage(accessor.get(IViewsService), accessor.get(IClipboardService)); + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + const clipboardService = serviceAccessor.get(IClipboardService); + const element = markersView.getFocusElement(); + if (element instanceof Marker) { + await clipboardService.writeText(element.marker.message); + } } }); -registerAction2(class extends Action2 { + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.RELATED_INFORMATION_COPY_MESSAGE_ACTION_ID, @@ -205,14 +237,20 @@ registerAction2(class extends Action2 { id: MenuId.ProblemsPanelContext, when: Constants.RelatedInformationFocusContextKey, group: 'navigation' - } + }, + viewId: Constants.MARKERS_VIEW_ID }); } - async run(accessor: ServicesAccessor) { - await copyRelatedInformationMessage(accessor.get(IViewsService), accessor.get(IClipboardService)); + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + const clipboardService = serviceAccessor.get(IClipboardService); + const element = markersView.getFocusElement(); + if (element instanceof RelatedInformation) { + await clipboardService.writeText(element.raw.message); + } } }); -registerAction2(class extends Action2 { + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.FOCUS_PROBLEMS_FROM_FILTER, @@ -221,14 +259,16 @@ registerAction2(class extends Action2 { when: Constants.MarkerViewFilterFocusContextKey, weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.DownArrow - } + }, + viewId: Constants.MARKERS_VIEW_ID }); } - run(accessor: ServicesAccessor) { - focusProblemsView(accessor.get(IViewsService)); + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + markersView.focus(); } }); -registerAction2(class extends Action2 { + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.MARKERS_VIEW_FOCUS_FILTER, @@ -237,14 +277,16 @@ registerAction2(class extends Action2 { when: FocusedViewContext.isEqualTo(Constants.MARKERS_VIEW_ID), weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.KEY_F - } + }, + viewId: Constants.MARKERS_VIEW_ID }); } - run(accessor: ServicesAccessor) { - focusProblemsFilter(accessor.get(IViewsService)); + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + markersView.focusFilter(); } }); -registerAction2(class extends Action2 { + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.MARKERS_VIEW_SHOW_MULTILINE_MESSAGE, @@ -253,17 +295,16 @@ registerAction2(class extends Action2 { menu: { id: MenuId.CommandPalette, when: ContextKeyExpr.has(getVisbileViewContextKey(Constants.MARKERS_VIEW_ID)) - } + }, + viewId: Constants.MARKERS_VIEW_ID }); } - run(accessor: ServicesAccessor) { - const markersView = accessor.get(IViewsService).getActiveViewWithId(Constants.MARKERS_VIEW_ID)!; - if (markersView) { - markersView.markersViewModel.multiline = true; - } + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + markersView.setMultiline(true); } }); -registerAction2(class extends Action2 { + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.MARKERS_VIEW_SHOW_SINGLELINE_MESSAGE, @@ -272,17 +313,16 @@ registerAction2(class extends Action2 { menu: { id: MenuId.CommandPalette, when: ContextKeyExpr.has(getVisbileViewContextKey(Constants.MARKERS_VIEW_ID)) - } + }, + viewId: Constants.MARKERS_VIEW_ID }); } - run(accessor: ServicesAccessor) { - const markersView = accessor.get(IViewsService).getActiveViewWithId(Constants.MARKERS_VIEW_ID); - if (markersView) { - markersView.markersViewModel.multiline = false; - } + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + markersView.setMultiline(false); } }); -registerAction2(class extends Action2 { + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.MARKERS_VIEW_CLEAR_FILTER_TEXT, @@ -291,76 +331,65 @@ registerAction2(class extends Action2 { keybinding: { when: Constants.MarkerViewFilterFocusContextKey, weight: KeybindingWeight.WorkbenchContrib, - } + }, + viewId: Constants.MARKERS_VIEW_ID }); } - run(accessor: ServicesAccessor) { - const markersView = accessor.get(IViewsService).getActiveViewWithId(Constants.MARKERS_VIEW_ID); - if (markersView) { - markersView.clearFilterText(); - } + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + markersView.clearFilterText(); } }); -async function copyMarker(viewsService: IViewsService, clipboardService: IClipboardService) { - const markersView = viewsService.getActiveViewWithId(Constants.MARKERS_VIEW_ID); - if (markersView) { - const element = markersView.getFocusElement(); - if (element instanceof Marker) { - await clipboardService.writeText(`${element}`); - } +registerAction2(class extends ViewAction { + constructor() { + super({ + id: `workbench.actions.treeView.${Constants.MARKERS_VIEW_ID}.collapseAll`, + title: localize('collapseAll', "Collapse All"), + menu: { + id: MenuId.ViewTitle, + when: ContextKeyEqualsExpr.create('view', Constants.MARKERS_VIEW_ID), + group: 'navigation', + order: 2, + }, + icon: Codicon.collapseAll, + viewId: Constants.MARKERS_VIEW_ID + }); } -} - -async function copyMessage(viewsService: IViewsService, clipboardService: IClipboardService) { - const markersView = viewsService.getActiveViewWithId(Constants.MARKERS_VIEW_ID); - if (markersView) { - const element = markersView.getFocusElement(); - if (element instanceof Marker) { - await clipboardService.writeText(element.marker.message); - } + async runInView(serviceAccessor: ServicesAccessor, view: IMarkersView): Promise { + return view.collapseAll(); } -} - -async function copyRelatedInformationMessage(viewsService: IViewsService, clipboardService: IClipboardService) { - const markersView = viewsService.getActiveViewWithId(Constants.MARKERS_VIEW_ID); - if (markersView) { - const element = markersView.getFocusElement(); - if (element instanceof RelatedInformation) { - await clipboardService.writeText(element.raw.message); - } - } -} - -function focusProblemsView(viewsService: IViewsService) { - const markersView = viewsService.getActiveViewWithId(Constants.MARKERS_VIEW_ID); - if (markersView) { - markersView.focus(); - } -} - -function focusProblemsFilter(viewsService: IViewsService): void { - const markersView = viewsService.getActiveViewWithId(Constants.MARKERS_VIEW_ID); - if (markersView) { - markersView.focusFilter(); - } -} - -MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '4_panels', - command: { - id: ToggleMarkersPanelAction.ID, - title: localize({ key: 'miMarker', comment: ['&& denotes a mnemonic'] }, "&&Problems") - }, - order: 4 }); -CommandsRegistry.registerCommand(Constants.TOGGLE_MARKERS_VIEW_ACTION_ID, async (accessor) => { - const viewsService = accessor.get(IViewsService); - if (viewsService.isViewVisible(Constants.MARKERS_VIEW_ID)) { - viewsService.closeView(Constants.MARKERS_VIEW_ID); - } else { - viewsService.openView(Constants.MARKERS_VIEW_ID, true); +registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.treeView.${Constants.MARKERS_VIEW_ID}.filter`, + title: localize('filter', "Filter"), + menu: { + id: MenuId.ViewTitle, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', Constants.MARKERS_VIEW_ID), Constants.MarkersViewSmallLayoutContextKey.negate()), + group: 'navigation', + order: 1, + }, + }); + } + async run(): Promise { } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: Constants.TOGGLE_MARKERS_VIEW_ACTION_ID, + title: Messages.MARKERS_PANEL_TOGGLE_LABEL, + }); + } + async run(accessor: ServicesAccessor): Promise { + const viewsService = accessor.get(IViewsService); + if (viewsService.isViewVisible(Constants.MARKERS_VIEW_ID)) { + viewsService.closeView(Constants.MARKERS_VIEW_ID); + } else { + viewsService.openView(Constants.MARKERS_VIEW_ID, true); + } } }); diff --git a/src/vs/workbench/contrib/markers/browser/markers.ts b/src/vs/workbench/contrib/markers/browser/markers.ts index 19623546..10592aba 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.ts @@ -9,6 +9,26 @@ import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/co import { localize } from 'vs/nls'; import Constants from './constants'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { MarkersFilters } from 'vs/workbench/contrib/markers/browser/markersViewActions'; +import { Event } from 'vs/base/common/event'; +import { IView } from 'vs/workbench/common/views'; +import { MarkerElement } from 'vs/workbench/contrib/markers/browser/markersModel'; + +export interface IMarkersView extends IView { + + readonly onDidFocusFilter: Event; + readonly onDidClearFilterText: Event; + readonly filters: MarkersFilters; + readonly onDidChangeFilterStats: Event<{ total: number, filtered: number }>; + focusFilter(): void; + clearFilterText(): void; + getFilterStats(): { total: number, filtered: number }; + + getFocusElement(): MarkerElement | undefined; + + collapseAll(): void; + setMultiline(multiline: boolean): void; +} export class ActivityUpdater extends Disposable implements IWorkbenchContribution { diff --git a/src/vs/workbench/contrib/markers/browser/markersModel.ts b/src/vs/workbench/contrib/markers/browser/markersModel.ts index a12e1658..794791b3 100644 --- a/src/vs/workbench/contrib/markers/browser/markersModel.ts +++ b/src/vs/workbench/contrib/markers/browser/markersModel.ts @@ -14,6 +14,7 @@ import { Hasher } from 'vs/base/common/hash'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { splitLines } from 'vs/base/common/strings'; +export type MarkerElement = ResourceMarkers | Marker | RelatedInformation; export function compareMarkersByUri(a: IMarker, b: IMarker) { return extUri.compare(a.resource, b.resource); diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 6a344c8a..653b012f 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -10,7 +10,7 @@ import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IMarker, MarkerSeverity } from 'vs/platform/markers/common/markers'; -import { ResourceMarkers, Marker, RelatedInformation } from 'vs/workbench/contrib/markers/browser/markersModel'; +import { ResourceMarkers, Marker, RelatedInformation, MarkerElement } from 'vs/workbench/contrib/markers/browser/markersModel'; import Messages from 'vs/workbench/contrib/markers/browser/messages'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; @@ -56,8 +56,6 @@ import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; -export type TreeElement = ResourceMarkers | Marker | RelatedInformation; - interface IResourceMarkersTemplateData { resourceLabel: IResourceLabel; count: CountBadge; @@ -74,7 +72,7 @@ interface IRelatedInformationTemplateData { description: HighlightedLabel; } -export class MarkersTreeAccessibilityProvider implements IListAccessibilityProvider { +export class MarkersTreeAccessibilityProvider implements IListAccessibilityProvider { constructor(@ILabelService private readonly labelService: ILabelService) { } @@ -82,7 +80,7 @@ export class MarkersTreeAccessibilityProvider implements IListAccessibilityProvi return localize('problemsView', "Problems View"); } - public getAriaLabel(element: TreeElement): string | null { + public getAriaLabel(element: MarkerElement): string | null { if (element instanceof ResourceMarkers) { const path = this.labelService.getUriLabel(element.resource, { relative: true }) || element.resource.fsPath; return Messages.MARKERS_TREE_ARIA_LABEL_RESOURCE(element.markers.length, element.name, paths.dirname(path)); @@ -103,13 +101,13 @@ const enum TemplateId { RelatedInformation = 'ri' } -export class VirtualDelegate implements IListVirtualDelegate { +export class VirtualDelegate implements IListVirtualDelegate { static LINE_HEIGHT: number = 22; constructor(private readonly markersViewState: MarkersViewModel) { } - getHeight(element: TreeElement): number { + getHeight(element: MarkerElement): number { if (element instanceof Marker) { const viewModel = this.markersViewState.getViewModel(element); const noOfLines = !viewModel || viewModel.multiline ? element.lines.length : 1; @@ -118,7 +116,7 @@ export class VirtualDelegate implements IListVirtualDelegate { return VirtualDelegate.LINE_HEIGHT; } - getTemplateId(element: TreeElement): string { + getTemplateId(element: MarkerElement): string { if (element instanceof ResourceMarkers) { return TemplateId.ResourceMarkers; } else if (element instanceof Marker) { @@ -512,11 +510,11 @@ export class RelatedInformationRenderer implements ITreeRenderer { +export class Filter implements ITreeFilter { constructor(public options: FilterOptions) { } - filter(element: TreeElement, parentVisibility: TreeVisibility): TreeFilterResult { + filter(element: MarkerElement, parentVisibility: TreeVisibility): TreeFilterResult { if (element instanceof ResourceMarkers) { return this.filterResourceMarkers(element); } else if (element instanceof Marker) { @@ -852,23 +850,23 @@ export class MarkersViewModel extends Disposable { } -export class ResourceDragAndDrop implements ITreeDragAndDrop { +export class ResourceDragAndDrop implements ITreeDragAndDrop { constructor( private instantiationService: IInstantiationService ) { } - onDragOver(data: IDragAndDropData, targetElement: TreeElement, targetIndex: number, originalEvent: DragEvent): boolean | ITreeDragOverReaction { + onDragOver(data: IDragAndDropData, targetElement: MarkerElement, targetIndex: number, originalEvent: DragEvent): boolean | ITreeDragOverReaction { return false; } - getDragURI(element: TreeElement): string | null { + getDragURI(element: MarkerElement): string | null { if (element instanceof ResourceMarkers) { return element.resource.toString(); } return null; } - getDragLabel?(elements: TreeElement[]): string | undefined { + getDragLabel?(elements: MarkerElement[]): string | undefined { if (elements.length > 1) { return String(elements.length); } @@ -877,7 +875,7 @@ export class ResourceDragAndDrop implements ITreeDragAndDrop { } onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void { - const elements = (data as ElementsDragAndDropData).elements; + const elements = (data as ElementsDragAndDropData).elements; const resources: URI[] = elements .filter(e => e instanceof ResourceMarkers) .map(resourceMarker => (resourceMarker as ResourceMarkers).resource); @@ -888,7 +886,7 @@ export class ResourceDragAndDrop implements ITreeDragAndDrop { } } - drop(data: IDragAndDropData, targetElement: TreeElement, targetIndex: number, originalEvent: DragEvent): void { + drop(data: IDragAndDropData, targetElement: MarkerElement, targetIndex: number, originalEvent: DragEvent): void { } } diff --git a/src/vs/workbench/contrib/markers/browser/markersView.ts b/src/vs/workbench/contrib/markers/browser/markersView.ts index 8836c837..b2a7ce0f 100644 --- a/src/vs/workbench/contrib/markers/browser/markersView.ts +++ b/src/vs/workbench/contrib/markers/browser/markersView.ts @@ -11,17 +11,17 @@ import { IAction, IActionViewItem, Action, Separator } from 'vs/base/common/acti import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import Constants from 'vs/workbench/contrib/markers/browser/constants'; -import { Marker, ResourceMarkers, RelatedInformation, MarkerChangesEvent, MarkersModel, compareMarkersByUri } from 'vs/workbench/contrib/markers/browser/markersModel'; +import { Marker, ResourceMarkers, RelatedInformation, MarkerChangesEvent, MarkersModel, compareMarkersByUri, MarkerElement } from 'vs/workbench/contrib/markers/browser/markersModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { MarkersFilterActionViewItem, MarkersFilters, IMarkersFiltersChangeEvent, IMarkerFilterController } from 'vs/workbench/contrib/markers/browser/markersViewActions'; +import { MarkersFilterActionViewItem, MarkersFilters, IMarkersFiltersChangeEvent } from 'vs/workbench/contrib/markers/browser/markersViewActions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import Messages from 'vs/workbench/contrib/markers/browser/messages'; -import { RangeHighlightDecorations } from 'vs/workbench/browser/parts/editor/rangeDecorations'; +import { RangeHighlightDecorations } from 'vs/workbench/browser/codeeditor'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { localize } from 'vs/nls'; -import { IContextKey, IContextKeyService, ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Iterable } from 'vs/base/common/iterator'; import { ITreeElement, ITreeNode, ITreeContextMenuEvent, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { Relay, Event, Emitter } from 'vs/base/common/event'; @@ -30,10 +30,10 @@ import { FilterOptions } from 'vs/workbench/contrib/markers/browser/markersFilte import { IExpression } from 'vs/base/common/glob'; import { deepClone } from 'vs/base/common/objects'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { FilterData, Filter, VirtualDelegate, ResourceMarkersRenderer, MarkerRenderer, RelatedInformationRenderer, TreeElement, MarkersTreeAccessibilityProvider, MarkersViewModel, ResourceDragAndDrop } from 'vs/workbench/contrib/markers/browser/markersTreeViewer'; +import { FilterData, Filter, VirtualDelegate, ResourceMarkersRenderer, MarkerRenderer, RelatedInformationRenderer, MarkersTreeAccessibilityProvider, MarkersViewModel, ResourceDragAndDrop } from 'vs/workbench/contrib/markers/browser/markersTreeViewer'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IMenuService, MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { domEvent } from 'vs/base/browser/event'; @@ -45,7 +45,7 @@ import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { KeyCode } from 'vs/base/common/keyCodes'; import { editorLightBulbForeground, editorLightBulbAutoFixForeground } from 'vs/platform/theme/common/colorRegistry'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { Codicon } from 'vs/base/common/codicons'; @@ -55,8 +55,9 @@ import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifec import { groupBy } from 'vs/base/common/arrays'; import { ResourceMap } from 'vs/base/common/map'; import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; +import { IMarkersView } from 'vs/workbench/contrib/markers/browser/markers'; -function createResourceMarkersIterator(resourceMarkers: ResourceMarkers): Iterable> { +function createResourceMarkersIterator(resourceMarkers: ResourceMarkers): Iterable> { return Iterable.map(resourceMarkers.markers, m => { const relatedInformationIt = Iterable.from(m.relatedInformation); const children = Iterable.map(relatedInformationIt, r => ({ element: r })); @@ -65,7 +66,7 @@ function createResourceMarkersIterator(resourceMarkers: ResourceMarkers): Iterab }); } -export class MarkersView extends ViewPane implements IMarkerFilterController { +export class MarkersView extends ViewPane implements IMarkersView { private lastSelectedRelativeTop: number = 0; private currentActiveResource: URI | null = null; @@ -88,7 +89,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { private cachedFilterStats: { total: number; filtered: number; } | undefined = undefined; private currentResourceGotAddedToMarkersData: boolean = false; - readonly markersViewModel: MarkersViewModel; + private readonly markersViewModel: MarkersViewModel; private readonly smallLayoutContextKey: IContextKey; private get smallLayout(): boolean { return !!this.smallLayoutContextKey.get(); } private set smallLayout(smallLayout: boolean) { this.smallLayoutContextKey.set(smallLayout); } @@ -132,8 +133,6 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { this.filter = new Filter(FilterOptions.EMPTY(uriIdentityService)); this.rangeHighlightDecorations = this._register(this.instantiationService.createInstance(RangeHighlightDecorations)); - // actions - this.regiserActions(); this.filters = this._register(new MarkersFilters({ filterText: this.panelState['filter'] || '', filterHistory: this.panelState['filterHistory'] || [], @@ -208,43 +207,6 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { this._onDidClearFilterText.fire(); } - private regiserActions(): void { - const that = this; - this._register(registerAction2(class extends Action2 { - constructor() { - super({ - id: `workbench.actions.treeView.${that.id}.collapseAll`, - title: localize('collapseAll', "Collapse All"), - menu: { - id: MenuId.ViewTitle, - when: ContextKeyEqualsExpr.create('view', that.id), - group: 'navigation', - order: Number.MAX_SAFE_INTEGER, - }, - icon: Codicon.collapseAll - }); - } - async run(): Promise { - return that.collapseAll(); - } - })); - this._register(registerAction2(class extends Action2 { - constructor() { - super({ - id: `workbench.actions.treeView.${that.id}.filter`, - title: localize('filter', "Filter"), - menu: { - id: MenuId.ViewTitle, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', that.id), Constants.MarkersViewSmallLayoutContextKey.negate()), - group: 'navigation', - order: 1, - }, - }); - } - async run(): Promise { } - })); - } - public showQuickFixes(marker: Marker): void { const viewModel = this.markersViewModel.getViewModel(marker); if (viewModel) { @@ -423,7 +385,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { const accessibilityProvider = this.instantiationService.createInstance(MarkersTreeAccessibilityProvider); const identityProvider = { - getId(element: TreeElement) { + getId(element: MarkerElement) { return element.id; } }; @@ -438,7 +400,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { accessibilityProvider, identityProvider, dnd: new ResourceDragAndDrop(this.instantiationService), - expandOnlyOnTwistieClick: (e: TreeElement) => e instanceof Marker && e.relatedInformation.length > 0, + expandOnlyOnTwistieClick: (e: MarkerElement) => e instanceof Marker && e.relatedInformation.length > 0, overrideStyles: { listBackground: this.getBackgroundColor() }, @@ -501,7 +463,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { this._register(this.tree.onDidChangeSelection(() => this.onSelected())); } - private collapseAll(): void { + collapseAll(): void { if (this.tree) { this.tree.collapseAll(); this.tree.setSelection([]); @@ -511,6 +473,10 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { } } + setMultiline(multiline: boolean): void { + this.markersViewModel.multiline = multiline; + } + private onDidChangeMarkersViewVisibility(visible: boolean): void { this.onVisibleDisposables.clear(); if (visible) { @@ -790,7 +756,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { this.rangeHighlightDecorations.highlightRange(selection); } - private onContextMenu(e: ITreeContextMenuEvent): void { + private onContextMenu(e: ITreeContextMenuEvent): void { const element = e.element; if (!element) { return; @@ -817,7 +783,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { }); } - private getMenuActions(element: TreeElement): IAction[] { + private getMenuActions(element: MarkerElement): IAction[] { const result: IAction[] = []; if (element instanceof Marker) { @@ -845,8 +811,8 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { return result; } - public getFocusElement() { - return this.tree ? this.tree.getFocus()[0] : undefined; + public getFocusElement(): MarkerElement | undefined { + return this.tree?.getFocus()[0] || undefined; } public getActionViewItem(action: IAction): IActionViewItem | undefined { @@ -924,14 +890,14 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { } -class MarkersTree extends WorkbenchObjectTree { +class MarkersTree extends WorkbenchObjectTree { constructor( user: string, readonly container: HTMLElement, - delegate: IListVirtualDelegate, - renderers: ITreeRenderer[], - options: IWorkbenchObjectTreeOptions, + delegate: IListVirtualDelegate, + renderers: ITreeRenderer[], + options: IWorkbenchObjectTreeOptions, @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, @IThemeService themeService: IThemeService, diff --git a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts index 973e9e69..7dacfc30 100644 --- a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts +++ b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts @@ -24,27 +24,11 @@ import { Marker } from 'vs/workbench/contrib/markers/browser/markersModel'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Event, Emitter } from 'vs/base/common/event'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; -import { IViewsService } from 'vs/workbench/common/views'; import { Codicon } from 'vs/base/common/codicons'; import { BaseActionViewItem, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; - -export class ShowProblemsPanelAction extends Action { - - public static readonly ID = 'workbench.action.problems.focus'; - public static readonly LABEL = Messages.MARKERS_PANEL_SHOW_LABEL; - - constructor(id: string, label: string, - @IViewsService private readonly viewsService: IViewsService - ) { - super(id, label); - } - - public run(): Promise { - return this.viewsService.openView(Constants.MARKERS_VIEW_ID, true); - } -} +import { IMarkersView } from 'vs/workbench/contrib/markers/browser/markers'; export interface IMarkersFiltersChangeEvent { filterText?: boolean; @@ -164,14 +148,6 @@ export class MarkersFilters extends Disposable { } } -export interface IMarkerFilterController { - readonly onDidFocusFilter: Event; - readonly onDidClearFilterText: Event; - readonly filters: MarkersFilters; - readonly onDidChangeFilterStats: Event<{ total: number, filtered: number }>; - getFilterStats(): { total: number, filtered: number }; -} - class FiltersDropdownMenuActionViewItem extends DropdownMenuActionViewItem { constructor( @@ -271,7 +247,7 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { constructor( action: IAction, - private filterController: IMarkerFilterController, + private markersView: IMarkersView, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextViewService private readonly contextViewService: IContextViewService, @IThemeService private readonly themeService: IThemeService, @@ -281,11 +257,11 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { this.focusContextKey = Constants.MarkerViewFilterFocusContextKey.bindTo(contextKeyService); this.delayedFilterUpdate = new Delayer(400); this._register(toDisposable(() => this.delayedFilterUpdate.cancel())); - this._register(filterController.onDidFocusFilter(() => this.focus())); - this._register(filterController.onDidClearFilterText(() => this.clearFilterText())); + this._register(markersView.onDidFocusFilter(() => this.focus())); + this._register(markersView.onDidClearFilterText(() => this.clearFilterText())); this.filtersAction = new Action('markersFiltersAction', Messages.MARKERS_PANEL_ACTION_TOOLTIP_MORE_FILTERS, 'markers-filters ' + ThemeIcon.asClassName(filterIcon)); this.filtersAction.checked = this.hasFiltersChanged(); - this._register(filterController.filters.onDidChange(e => this.onDidFiltersChange(e))); + this._register(markersView.filters.onDidChange(e => this.onDidFiltersChange(e))); } render(container: HTMLElement): void { @@ -321,21 +297,21 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { } private hasFiltersChanged(): boolean { - return !this.filterController.filters.showErrors || !this.filterController.filters.showWarnings || !this.filterController.filters.showInfos || this.filterController.filters.excludedFiles || this.filterController.filters.activeFile; + return !this.markersView.filters.showErrors || !this.markersView.filters.showWarnings || !this.markersView.filters.showInfos || this.markersView.filters.excludedFiles || this.markersView.filters.activeFile; } private createInput(container: HTMLElement): void { this.filterInputBox = this._register(this.instantiationService.createInstance(ContextScopedHistoryInputBox, container, this.contextViewService, { placeholder: Messages.MARKERS_PANEL_FILTER_PLACEHOLDER, ariaLabel: Messages.MARKERS_PANEL_FILTER_ARIA_LABEL, - history: this.filterController.filters.filterHistory + history: this.markersView.filters.filterHistory })); this._register(attachInputBoxStyler(this.filterInputBox, this.themeService)); - this.filterInputBox.value = this.filterController.filters.filterText; + this.filterInputBox.value = this.markersView.filters.filterText; this._register(this.filterInputBox.onDidChange(filter => this.delayedFilterUpdate.trigger(() => this.onDidInputChange(this.filterInputBox!)))); - this._register(this.filterController.filters.onDidChange((event: IMarkersFiltersChangeEvent) => { + this._register(this.markersView.filters.onDidChange((event: IMarkersFiltersChangeEvent) => { if (event.filterText) { - this.filterInputBox!.value = this.filterController.filters.filterText; + this.filterInputBox!.value = this.markersView.filters.filterText; } })); this._register(DOM.addStandardDisposableListener(this.filterInputBox.inputElement, DOM.EventType.KEY_DOWN, (e: any) => this.onInputKeyDown(e, this.filterInputBox!))); @@ -373,14 +349,14 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { filterBadge.style.color = foreground; })); this.updateBadge(); - this._register(this.filterController.onDidChangeFilterStats(() => this.updateBadge())); + this._register(this.markersView.onDidChangeFilterStats(() => this.updateBadge())); } private createFilters(container: HTMLElement): void { const actionbar = this._register(new ActionBar(container, { actionViewItemProvider: action => { if (action.id === this.filtersAction.id) { - return this.instantiationService.createInstance(FiltersDropdownMenuActionViewItem, action, this.filterController.filters, this.actionRunner); + return this.instantiationService.createInstance(FiltersDropdownMenuActionViewItem, action, this.markersView.filters, this.actionRunner); } return undefined; } @@ -390,13 +366,13 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { private onDidInputChange(inputbox: HistoryInputBox) { inputbox.addToHistory(); - this.filterController.filters.filterText = inputbox.value; - this.filterController.filters.filterHistory = inputbox.getHistory(); + this.markersView.filters.filterText = inputbox.value; + this.markersView.filters.filterHistory = inputbox.getHistory(); } private updateBadge(): void { if (this.filterBadge) { - const { total, filtered } = this.filterController.getFilterStats(); + const { total, filtered } = this.markersView.getFilterStats(); this.filterBadge.classList.toggle('hidden', total === filtered || total === 0); this.filterBadge.textContent = localize('showing filtered problems', "Showing {0} of {1}", filtered, total); this.adjustInputBox(); @@ -441,9 +417,9 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { } protected get class(): string { - if (this.filterController.filters.layout.width > 600) { + if (this.markersView.filters.layout.width > 600) { return 'markers-panel-action-filter grow'; - } else if (this.filterController.filters.layout.width < 400) { + } else if (this.markersView.filters.layout.width < 400) { return 'markers-panel-action-filter small'; } else { return 'markers-panel-action-filter'; diff --git a/src/vs/workbench/contrib/markers/browser/messages.ts b/src/vs/workbench/contrib/markers/browser/messages.ts index 5d710125..d263f7e1 100644 --- a/src/vs/workbench/contrib/markers/browser/messages.ts +++ b/src/vs/workbench/contrib/markers/browser/messages.ts @@ -19,8 +19,8 @@ export default class Messages { public static MARKERS_PANEL_TITLE_PROBLEMS: string = nls.localize('markers.panel.title.problems', "Problems"); - public static MARKERS_PANEL_NO_PROBLEMS_BUILT: string = nls.localize('markers.panel.no.problems.build', "No problems have been detected in the workspace so far."); - public static MARKERS_PANEL_NO_PROBLEMS_ACTIVE_FILE_BUILT: string = nls.localize('markers.panel.no.problems.activeFile.build', "No problems have been detected in the current file so far."); + public static MARKERS_PANEL_NO_PROBLEMS_BUILT: string = nls.localize('markers.panel.no.problems.build', "No problems have been detected in the workspace."); + public static MARKERS_PANEL_NO_PROBLEMS_ACTIVE_FILE_BUILT: string = nls.localize('markers.panel.no.problems.activeFile.build', "No problems have been detected in the current file."); public static MARKERS_PANEL_NO_PROBLEMS_FILTERS: string = nls.localize('markers.panel.no.problems.filters', "No results found with provided filter criteria."); public static MARKERS_PANEL_ACTION_TOOLTIP_MORE_FILTERS: string = nls.localize('markers.panel.action.moreFilters', "More Filters..."); diff --git a/src/vs/workbench/contrib/notebook/browser/constants.ts b/src/vs/workbench/contrib/notebook/browser/constants.ts index 3232b047..ab61c29e 100644 --- a/src/vs/workbench/contrib/notebook/browser/constants.ts +++ b/src/vs/workbench/contrib/notebook/browser/constants.ts @@ -13,8 +13,8 @@ export const CELL_RUN_GUTTER = 28; export const CODE_CELL_LEFT_MARGIN = 32; export const EDITOR_TOOLBAR_HEIGHT = 0; -export const BOTTOM_CELL_TOOLBAR_GAP = 18; -export const BOTTOM_CELL_TOOLBAR_HEIGHT = 50; +export const BOTTOM_CELL_TOOLBAR_GAP = 16; +export const BOTTOM_CELL_TOOLBAR_HEIGHT = 24; export const CELL_STATUSBAR_HEIGHT = 22; // Margin above editor diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index acf32d1a..9fb71481 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -23,11 +23,17 @@ import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/ import { CATEGORIES } from 'vs/workbench/common/actions'; import { BaseCellRenderTemplate, CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID, EXPAND_CELL_CONTENT_COMMAND_ID, IActiveNotebookEditor, ICellViewModel, INotebookEditor, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellEditType, CellKind, ICellEditOperation, ICellRange, isDocumentExcludePattern, NotebookCellMetadata, NotebookCellRunState, NOTEBOOK_EDITOR_CURSOR_BEGIN_END, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellKind, ICellEditOperation, ICellRange, INotebookDocumentFilter, isDocumentExcludePattern, NotebookCellMetadata, NotebookCellRunState, NOTEBOOK_EDITOR_CURSOR_BEGIN_END, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; +import { EditorsOrder } from 'vs/workbench/common/editor'; +import { INotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; // Notebook Commands const EXECUTE_NOTEBOOK_COMMAND_ID = 'notebook.execute'; @@ -137,14 +143,17 @@ abstract class NotebookAction extends Action2 { super(desc); } - async run(accessor: ServicesAccessor, context?: INotebookActionContext): Promise { + async run(accessor: ServicesAccessor, context?: any): Promise { if (!this.isNotebookActionContext(context)) { - context = this.getActiveEditorContext(accessor); + context = this.getEditorContextFromArgsOrActive(accessor, context); if (!context) { return; } } + const telemetryService = accessor.get(ITelemetryService); + telemetryService.publicLog2('workbenchActionExecuted', { id: this.desc.id, from: 'ui' }); + this.runWithContext(accessor, context); } @@ -154,7 +163,7 @@ abstract class NotebookAction extends Action2 { return !!context && !!(context as INotebookActionContext).notebookEditor; } - protected getActiveEditorContext(accessor: ServicesAccessor): INotebookActionContext | undefined { + protected getEditorContextFromArgsOrActive(accessor: ServicesAccessor, context?: any): INotebookActionContext | undefined { const editorService = accessor.get(IEditorService); const editor = getActiveNotebookEditor(editorService); @@ -179,22 +188,22 @@ abstract class NotebookCellAction extends Notebo return !!context && !!(context as INotebookCellActionContext).notebookEditor && !!(context as INotebookCellActionContext).cell; } - protected getCellContextFromArgs(accessor: ServicesAccessor, context?: T): INotebookCellActionContext | undefined { + protected getCellContextFromArgs(accessor: ServicesAccessor, context?: T, ...additionalArgs: any[]): INotebookCellActionContext | undefined { return undefined; } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { + async run(accessor: ServicesAccessor, context?: INotebookCellActionContext, ...additionalArgs: any[]): Promise { if (this.isCellActionContext(context)) { return this.runWithContext(accessor, context); } - const contextFromArgs = this.getCellContextFromArgs(accessor, context); + const contextFromArgs = this.getCellContextFromArgs(accessor, context, ...additionalArgs); if (contextFromArgs) { return this.runWithContext(accessor, contextFromArgs); } - const activeEditorContext = this.getActiveEditorContext(accessor); + const activeEditorContext = this.getEditorContextFromArgsOrActive(accessor); if (this.isCellActionContext(activeEditorContext)) { return this.runWithContext(accessor, activeEditorContext); } @@ -203,6 +212,22 @@ abstract class NotebookCellAction extends Notebo abstract runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise; } +export function getWidgetFromUri(accessor: ServicesAccessor, uri: URI) { + const editorService = accessor.get(IEditorService); + const notebookWidgetService = accessor.get(INotebookEditorWidgetService); + const editorId = editorService.getEditors(EditorsOrder.SEQUENTIAL).find(editorId => editorId.editor instanceof NotebookEditorInput && editorId.editor.resource?.toString() === uri.toString()); + + if (editorId) { + const widget = notebookWidgetService.widgets.find(widget => widget.textModel?.viewType === (editorId.editor as NotebookEditorInput).viewType && widget.uri?.toString() === editorId.editor.resource!.toString()); + + if (widget && widget.hasModel()) { + return widget; + } + } + + return undefined; +} + registerAction2(class extends NotebookCellAction { constructor() { super({ @@ -234,6 +259,11 @@ registerAction2(class extends NotebookCellAction { } } } + }, + { + name: 'uri', + description: 'The document uri', + constraint: URI } ] }, @@ -241,12 +271,28 @@ registerAction2(class extends NotebookCellAction { }); } - getCellContextFromArgs(accessor: ServicesAccessor, context?: ICellRange): INotebookCellActionContext | undefined { + getCellContextFromArgs(accessor: ServicesAccessor, context?: ICellRange, ...additionalArgs: any[]): INotebookCellActionContext | undefined { if (!context || typeof context.start !== 'number' || typeof context.end !== 'number' || context.start >= context.end) { return; } - const activeEditorContext = this.getActiveEditorContext(accessor); + if (additionalArgs.length && additionalArgs[0]) { + const uri = URI.revive(additionalArgs[0]); + + if (uri) { + const widget = getWidgetFromUri(accessor, uri); + if (widget) { + const cells = widget.viewModel.viewCells; + + return { + notebookEditor: widget, + cell: cells[context.start] + }; + } + } + } + + const activeEditorContext = this.getEditorContextFromArgsOrActive(accessor); if (!activeEditorContext || !activeEditorContext.notebookEditor.viewModel || context.start >= activeEditorContext.notebookEditor.viewModel.viewCells.length) { return; @@ -273,7 +319,7 @@ registerAction2(class extends NotebookCellAction { title: localize('notebookActions.cancel', "Stop Cell Execution"), icon: icons.stopIcon, description: { - description: localize('notebookActions.execute', "Execute Cell"), + description: localize('notebookActions.cancel', "Stop Cell Execution"), args: [ { name: 'range', @@ -290,18 +336,39 @@ registerAction2(class extends NotebookCellAction { } } } + }, + { + name: 'uri', + description: 'The document uri', + constraint: URI } ] }, }); } - getCellContextFromArgs(accessor: ServicesAccessor, context?: ICellRange): INotebookCellActionContext | undefined { + getCellContextFromArgs(accessor: ServicesAccessor, context?: ICellRange, ...additionalArgs: any[]): INotebookCellActionContext | undefined { if (!context || typeof context.start !== 'number' || typeof context.end !== 'number' || context.start >= context.end) { return; } - const activeEditorContext = this.getActiveEditorContext(accessor); + if (additionalArgs.length && additionalArgs[0]) { + const uri = URI.revive(additionalArgs[0]); + + if (uri) { + const widget = getWidgetFromUri(accessor, uri); + if (widget) { + const cells = widget.viewModel.viewCells; + + return { + notebookEditor: widget, + cell: cells[context.start] + }; + } + } + } + + const activeEditorContext = this.getEditorContextFromArgsOrActive(accessor); if (!activeEditorContext || !activeEditorContext.notebookEditor.viewModel || context.start >= activeEditorContext.notebookEditor.viewModel.viewCells.length) { return; @@ -455,19 +522,62 @@ registerAction2(class extends NotebookAction { super({ id: EXECUTE_NOTEBOOK_COMMAND_ID, title: localize('notebookActions.executeNotebook', "Execute Notebook"), + description: { + description: localize('notebookActions.executeNotebook', "Execute Notebook"), + args: [ + { + name: 'uri', + description: 'The document uri', + constraint: URI + } + ] + }, }); } + getEditorContextFromArgsOrActive(accessor: ServicesAccessor, context?: UriComponents): INotebookActionContext | undefined { + if (context) { + const uri = URI.revive(context); + + if (uri) { + const widget = getWidgetFromUri(accessor, uri); + + if (widget) { + return { + notebookEditor: widget, + }; + } + } + } + + const editorService = accessor.get(IEditorService); + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return; + } + + if (!editor.hasModel()) { + return; + } + + const activeCell = editor.getActiveCell(); + return { + cell: activeCell, + notebookEditor: editor + }; + } + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { renderAllMarkdownCells(context); + const editorService = accessor.get(IEditorService); + const editor = editorService.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).find( + editor => editor.editor instanceof NotebookEditorInput && editor.editor.viewType === context.notebookEditor.viewModel.viewType && editor.editor.resource.toString() === context.notebookEditor.viewModel.uri.toString()); const editorGroupService = accessor.get(IEditorGroupsService); - const group = editorGroupService.activeGroup; - if (group) { - if (group.activeEditor) { - group.pinEditor(group.activeEditor); - } + if (editor) { + const group = editorGroupService.getGroup(editor.groupId); + group?.pinEditor(editor.editor); } return context.notebookEditor.executeNotebook(); @@ -487,9 +597,51 @@ registerAction2(class extends NotebookAction { super({ id: CANCEL_NOTEBOOK_COMMAND_ID, title: localize('notebookActions.cancelNotebook', "Cancel Notebook Execution"), + description: { + description: localize('notebookActions.cancelNotebook', "Cancel Notebook Execution"), + args: [ + { + name: 'uri', + description: 'The document uri', + constraint: URI + } + ] + }, }); } + getEditorContextFromArgsOrActive(accessor: ServicesAccessor, context?: UriComponents): INotebookActionContext | undefined { + if (context) { + const uri = URI.revive(context); + + if (uri) { + const widget = getWidgetFromUri(accessor, uri); + + if (widget) { + return { + notebookEditor: widget, + }; + } + } + } + + const editorService = accessor.get(IEditorService); + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return; + } + + if (!editor.hasModel()) { + return; + } + + const activeCell = editor.getActiveCell(); + return { + cell: activeCell, + notebookEditor: editor + }; + } + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { return context.notebookEditor.cancelNotebookExecution(); } @@ -580,7 +732,7 @@ registerAction2(class extends NotebookCellAction { export function getActiveNotebookEditor(editorService: IEditorService): INotebookEditor | undefined { // TODO@roblourens can `isNotebookEditor` be on INotebookEditor to avoid a circular dependency? - const activeEditorPane = editorService.activeEditorPane as unknown as { isNotebookEditor?: boolean } | undefined; + const activeEditorPane = editorService.activeEditorPane as unknown as { isNotebookEditor?: boolean; } | undefined; return activeEditorPane?.isNotebookEditor ? (editorService.activeEditorPane?.getControl() as INotebookEditor) : undefined; } @@ -709,7 +861,7 @@ registerAction2(class extends NotebookAction { } async run(accessor: ServicesAccessor): Promise { - const context = this.getActiveEditorContext(accessor); + const context = this.getEditorContextFromArgsOrActive(accessor); if (context) { this.runWithContext(accessor, context); } @@ -734,7 +886,7 @@ registerAction2(class extends NotebookAction { } async run(accessor: ServicesAccessor): Promise { - const context = this.getActiveEditorContext(accessor); + const context = this.getEditorContextFromArgsOrActive(accessor); if (context) { this.runWithContext(accessor, context); } @@ -1891,8 +2043,8 @@ CommandsRegistry.registerCommand('notebook.trust', (accessor, args) => { CommandsRegistry.registerCommand('_resolveNotebookContentProvider', (accessor, args): { viewType: string; displayName: string; - options: { transientOutputs: boolean; transientMetadata: TransientMetadata }; - filenamePattern: (string | glob.IRelativePattern | { include: string | glob.IRelativePattern, exclude: string | glob.IRelativePattern })[] + options: { transientOutputs: boolean; transientMetadata: TransientMetadata; }; + filenamePattern: (string | glob.IRelativePattern | { include: string | glob.IRelativePattern, exclude: string | glob.IRelativePattern; })[]; }[] => { const notebookService = accessor.get(INotebookService); const contentProviders = notebookService.getContributedNotebookProviders(); @@ -1914,7 +2066,7 @@ CommandsRegistry.registerCommand('_resolveNotebookContentProvider', (accessor, a } return null; - }).filter(pattern => pattern !== null) as (string | glob.IRelativePattern | { include: string | glob.IRelativePattern, exclude: string | glob.IRelativePattern })[]; + }).filter(pattern => pattern !== null) as (string | glob.IRelativePattern | { include: string | glob.IRelativePattern, exclude: string | glob.IRelativePattern; })[]; return { viewType: provider.id, @@ -1924,3 +2076,44 @@ CommandsRegistry.registerCommand('_resolveNotebookContentProvider', (accessor, a }; }); }); + +CommandsRegistry.registerCommand('_resolveNotebookKernelProviders', async (accessor, args): Promise<{ + extensionId: string; + description?: string; + selector: INotebookDocumentFilter; +}[]> => { + const notebookService = accessor.get(INotebookService); + const providers = await notebookService.getContributedNotebookKernelProviders(); + return providers.map(provider => ({ + extensionId: provider.providerExtensionId, + description: provider.providerDescription, + selector: provider.selector + })); +}); + +CommandsRegistry.registerCommand('_resolveNotebookKernels', async (accessor, args: { + viewType: string; + uri: UriComponents; +}): Promise<{ + id?: string; + label: string; + description?: string; + detail?: string; + isPreferred?: boolean; + preloads?: URI[]; +}[]> => { + const notebookService = accessor.get(INotebookService); + const uri = URI.revive(args.uri as UriComponents); + const source = new CancellationTokenSource(); + const kernels = await notebookService.getContributedNotebookKernels(args.viewType, uri, source.token); + source.dispose(); + + return kernels.map(provider => ({ + id: provider.friendlyId, + label: provider.label, + description: provider.description, + detail: provider.detail, + isPreferred: provider.isPreferred, + preloads: provider.preloads?.map(preload => URI.revive(preload)) || [] + })); +}); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts index 2c2c46b8..c7f0f53c 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/notebookFind'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, INotebookEditor, CellFindMatch, CellEditState, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, INotebookEditor, CellFindMatch, CellEditState, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_OPEN } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { FindDecorations } from 'vs/editor/contrib/find/findDecorations'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; @@ -385,7 +385,7 @@ registerAction2(class extends Action2 { id: 'notebook.find', title: { value: localize('notebookActions.findInNotebook', "Find in Notebook"), original: 'Find in Notebook' }, keybinding: { - when: NOTEBOOK_EDITOR_FOCUSED, + when: ContextKeyExpr.or(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_OPEN), primary: KeyCode.KEY_F | KeyMod.CtrlCmd, weight: KeybindingWeight.WorkbenchContrib } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.css b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.css new file mode 100644 index 00000000..6e71e660 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.css @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-list .notebook-outline-element { + display: flex; + flex: 1; + flex-flow: row nowrap; + align-items: center; +} + +.monaco-list .notebook-outline-element > .element-icon.file-icon { + height: 100%; +} + +.monaco-breadcrumbs > .notebook-outline-element > .element-icon.file-icon { + height: 18px; +} +.monaco-list .notebook-outline-element .monaco-highlighted-label { + color: var(--outline-element-color); +} + +.monaco-breadcrumbs .notebook-outline-element .element-decoration, +.monaco-list .notebook-outline-element > .element-decoration { + opacity: 0.75; + font-size: 90%; + font-weight: 600; + padding: 0 12px 0 5px; + margin-left: auto; + text-align: center; + color: var(--outline-element-color); +} + +.monaco-list .notebook-outline-element > .element-decoration.bubble { + font-family: codicon; + font-size: 14px; + opacity: 0.4; + padding-right: 8px; +} + +.monaco-breadcrumbs .notebook-outline-element .element-decoration { + /* Don't show markers inline with breadcrumbs */ + display: none; +} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts new file mode 100644 index 00000000..f6c2dd20 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -0,0 +1,607 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./notebookOutline'; +import { Codicon } from 'vs/base/common/codicons'; +import { Emitter, Event } from 'vs/base/common/event'; +import { combinedDisposable, IDisposable, Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IOutline, IOutlineComparator, IOutlineCreator, IOutlineListConfig, IOutlineService, IQuickPickDataSource, IQuickPickOutlineElement, OutlineChangeEvent, OutlineConfigKeys, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IEditorPane } from 'vs/workbench/common/editor'; +import { IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { createMatches, FuzzyScore } from 'vs/base/common/filters'; +import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { getIconClassesForModeId } from 'vs/editor/common/services/getIconClasses'; +import { IWorkbenchDataTreeOptions } from 'vs/platform/list/browser/listService'; +import { localize } from 'vs/nls'; +import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { listErrorForeground, listWarningForeground } from 'vs/platform/theme/common/colorRegistry'; +import { isEqual } from 'vs/base/common/resources'; +import { IdleValue } from 'vs/base/common/async'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; +import * as marked from 'vs/base/common/marked/marked'; +import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; + +export interface IOutlineMarkerInfo { + readonly count: number; + readonly topSev: MarkerSeverity; +} + +export class OutlineEntry { + + private _children: OutlineEntry[] = []; + private _parent: OutlineEntry | undefined; + private _markerInfo: IOutlineMarkerInfo | undefined; + + constructor( + readonly index: number, + readonly level: number, + readonly cell: ICellViewModel, + readonly label: string, + readonly icon: ThemeIcon + ) { } + + addChild(entry: OutlineEntry) { + this._children.push(entry); + entry._parent = this; + } + + get parent(): OutlineEntry | undefined { + return this._parent; + } + + get children(): Iterable { + return this._children; + } + + get markerInfo(): IOutlineMarkerInfo | undefined { + return this._markerInfo; + } + + updateMarkers(markerService: IMarkerService): void { + if (this.cell.cellKind === CellKind.Code) { + // a code cell can have marker + const marker = markerService.read({ resource: this.cell.uri, severities: MarkerSeverity.Error | MarkerSeverity.Warning }); + if (marker.length === 0) { + this._markerInfo = undefined; + } else { + const topSev = marker.find(a => a.severity === MarkerSeverity.Error)?.severity ?? MarkerSeverity.Warning; + this._markerInfo = { topSev, count: marker.length }; + } + } else { + // a markdown cell can inherit markers from its children + let topChild: MarkerSeverity | undefined; + for (let child of this.children) { + child.updateMarkers(markerService); + if (child.markerInfo) { + topChild = !topChild ? child.markerInfo.topSev : Math.max(child.markerInfo.topSev, topChild); + } + } + this._markerInfo = topChild && { topSev: topChild, count: 0 }; + } + } + + clearMarkers(): void { + this._markerInfo = undefined; + for (let child of this.children) { + child.clearMarkers(); + } + } + + find(cell: ICellViewModel, parents: OutlineEntry[]): OutlineEntry | undefined { + if (cell.id === this.cell.id) { + return this; + } + parents.push(this); + for (let child of this.children) { + const result = child.find(cell, parents); + if (result) { + return result; + } + } + parents.pop(); + return undefined; + } + + asFlatList(bucket: OutlineEntry[]): void { + bucket.push(this); + for (let child of this.children) { + child.asFlatList(bucket); + } + } +} + +class NotebookOutlineTemplate { + + static readonly templateId = 'NotebookOutlineRenderer'; + + constructor( + readonly container: HTMLElement, + readonly iconClass: HTMLElement, + readonly iconLabel: IconLabel, + readonly decoration: HTMLElement + ) { } +} + +class NotebookOutlineRenderer implements ITreeRenderer { + + templateId: string = NotebookOutlineTemplate.templateId; + + constructor( + @IThemeService private readonly _themeService: IThemeService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { } + + renderTemplate(container: HTMLElement): NotebookOutlineTemplate { + container.classList.add('notebook-outline-element', 'show-file-icons'); + const iconClass = document.createElement('div'); + container.append(iconClass); + const iconLabel = new IconLabel(container, { supportHighlights: true }); + const decoration = document.createElement('div'); + decoration.className = 'element-decoration'; + container.append(decoration); + return new NotebookOutlineTemplate(container, iconClass, iconLabel, decoration); + } + + renderElement(node: ITreeNode, _index: number, template: NotebookOutlineTemplate, _height: number | undefined): void { + const options: IIconLabelValueOptions = { + matches: createMatches(node.filterData), + extraClasses: [] + }; + + if (node.element.cell.cellKind === CellKind.Code && this._themeService.getFileIconTheme().hasFileIcons) { + template.iconClass.className = ''; + options.extraClasses?.push(...getIconClassesForModeId(node.element.cell.language ?? '')); + } else { + template.iconClass.className = 'element-icon ' + ThemeIcon.asClassNameArray(node.element.icon).join(' '); + } + + template.iconLabel.setLabel(node.element.label, undefined, options); + + const { markerInfo } = node.element; + + template.container.style.removeProperty('--outline-element-color'); + template.decoration.innerText = ''; + if (markerInfo) { + const useBadges = this._configurationService.getValue(OutlineConfigKeys.problemsBadges); + if (!useBadges) { + template.decoration.classList.remove('bubble'); + template.decoration.innerText = ''; + } else if (markerInfo.count === 0) { + template.decoration.classList.add('bubble'); + template.decoration.innerText = '\uea71'; + } else { + template.decoration.classList.remove('bubble'); + template.decoration.innerText = markerInfo.count > 9 ? '9+' : String(markerInfo.count); + } + const color = this._themeService.getColorTheme().getColor(markerInfo.topSev === MarkerSeverity.Error ? listErrorForeground : listWarningForeground); + const useColors = this._configurationService.getValue(OutlineConfigKeys.problemsColors); + if (!useColors) { + template.container.style.removeProperty('--outline-element-color'); + template.decoration.style.setProperty('--outline-element-color', color?.toString() ?? 'inherit'); + } else { + template.container.style.setProperty('--outline-element-color', color?.toString() ?? 'inherit'); + } + } + } + + disposeTemplate(templateData: NotebookOutlineTemplate): void { + templateData.iconLabel.dispose(); + } +} + +class NotebookOutlineAccessibility implements IListAccessibilityProvider { + getAriaLabel(element: OutlineEntry): string | null { + return element.label; + } + getWidgetAriaLabel(): string { + return ''; + } +} + +class NotebookNavigationLabelProvider implements IKeyboardNavigationLabelProvider { + getKeyboardNavigationLabel(element: OutlineEntry): { toString(): string | undefined; } | { toString(): string | undefined; }[] | undefined { + return element.label; + } +} + +class NotebookOutlineVirtualDelegate implements IListVirtualDelegate { + + getHeight(_element: OutlineEntry): number { + return 22; + } + + getTemplateId(_element: OutlineEntry): string { + return NotebookOutlineTemplate.templateId; + } +} + +class NotebookQuickPickProvider implements IQuickPickDataSource { + + constructor( + private _getEntries: () => OutlineEntry[], + @IThemeService private readonly _themeService: IThemeService + ) { } + + getQuickPickElements(): Iterable> { + const bucket: OutlineEntry[] = []; + for (let entry of this._getEntries()) { + entry.asFlatList(bucket); + } + const result: IQuickPickOutlineElement[] = []; + const { hasFileIcons } = this._themeService.getFileIconTheme(); + for (let element of bucket) { + // todo@jrieken it is fishy that codicons cannot be used with iconClasses + // but file icons can... + result.push({ + element, + label: hasFileIcons ? element.label : `$(${element.icon.id}) ${element.label}`, + ariaLabel: element.label, + iconClasses: hasFileIcons ? getIconClassesForModeId(element.cell.language ?? '') : undefined, + }); + } + return result; + } +} + +class NotebookComparator implements IOutlineComparator { + + private readonly _collator = new IdleValue(() => new Intl.Collator(undefined, { numeric: true })); + + compareByPosition(a: OutlineEntry, b: OutlineEntry): number { + return a.index - b.index; + } + compareByType(a: OutlineEntry, b: OutlineEntry): number { + return a.cell.cellKind - b.cell.cellKind || this._collator.value.compare(a.label, b.label); + } + compareByName(a: OutlineEntry, b: OutlineEntry): number { + return this._collator.value.compare(a.label, b.label); + } +} + +class NotebookCellOutline implements IOutline { + + private readonly _dispoables = new DisposableStore(); + + private readonly _onDidChange = new Emitter(); + + readonly onDidChange: Event = this._onDidChange.event; + + private _entries: OutlineEntry[] = []; + private _activeEntry?: OutlineEntry; + private readonly _entriesDisposables = new DisposableStore(); + + readonly config: IOutlineListConfig; + readonly outlineKind = 'notebookCells'; + + get activeElement(): OutlineEntry | undefined { + return this._activeEntry; + } + + constructor( + private readonly _editor: NotebookEditor, + private readonly _target: OutlineTarget, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IEditorService private readonly _editorService: IEditorService, + @IMarkerService private readonly _markerService: IMarkerService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { + const selectionListener = new MutableDisposable(); + this._dispoables.add(selectionListener); + const installSelectionListener = () => { + if (!_editor.viewModel) { + selectionListener.clear(); + } else { + selectionListener.value = combinedDisposable( + _editor.viewModel.onDidChangeSelection(() => this._recomputeActive()), + _editor.viewModel.onDidChangeViewCells(() => this._recomputeState()) + ); + } + }; + + this._dispoables.add(_editor.onDidChangeModel(() => { + this._recomputeState(); + installSelectionListener(); + })); + + this._dispoables.add(_configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('notebook.outline.showCodeCells')) { + this._recomputeState(); + } + })); + + this._dispoables.add(themeService.onDidFileIconThemeChange(() => { + this._onDidChange.fire({}); + })); + + this._recomputeState(); + installSelectionListener(); + + const options: IWorkbenchDataTreeOptions = { + collapseByDefault: _target === OutlineTarget.Breadcrumbs, + expandOnlyOnTwistieClick: true, + multipleSelectionSupport: false, + accessibilityProvider: new NotebookOutlineAccessibility(), + identityProvider: { getId: element => element.cell.id }, + keyboardNavigationLabelProvider: new NotebookNavigationLabelProvider() + }; + + const treeDataSource: IDataSource = { getChildren: parent => parent instanceof NotebookCellOutline ? this._entries : parent.children }; + const delegate = new NotebookOutlineVirtualDelegate(); + const renderers = [instantiationService.createInstance(NotebookOutlineRenderer)]; + const comparator = new NotebookComparator(); + + this.config = { + breadcrumbsDataSource: { + getBreadcrumbElements: () => { + let result: OutlineEntry[] = []; + let candidate = this._activeEntry; + while (candidate) { + result.unshift(candidate); + candidate = candidate.parent; + } + return result; + } + }, + quickPickDataSource: instantiationService.createInstance(NotebookQuickPickProvider, () => this._entries), + treeDataSource, + delegate, + renderers, + comparator, + options + }; + } + + dispose(): void { + this._dispoables.dispose(); + this._entriesDisposables.dispose(); + } + + private _recomputeState(): void { + this._entriesDisposables.clear(); + this._activeEntry = undefined; + this._entries.length = 0; + + const { viewModel } = this._editor; + if (!viewModel) { + return; + } + + let includeCodeCells = true; + if (this._target === OutlineTarget.OutlinePane) { + includeCodeCells = this._configurationService.getValue('notebook.outline.showCodeCells'); + } else if (this._target === OutlineTarget.Breadcrumbs) { + includeCodeCells = this._configurationService.getValue('notebook.breadcrumbs.showCodeCells'); + } + + const [selected] = viewModel.selectionHandles; + const entries: OutlineEntry[] = []; + + for (let i = 0; i < viewModel.viewCells.length; i++) { + const cell = viewModel.viewCells[i]; + const isMarkdown = cell.cellKind === CellKind.Markdown; + if (!isMarkdown && !includeCodeCells) { + continue; + } + + // anaslse cell text but cap it 10000 characters + let content = cell.getText().substr(0, 10_000); + let level = 7; + + if (isMarkdown) { + // MD cell: "render" as plain text, find highest header + for (const token of marked.lexer(content, { gfm: true })) { + if (token.type === 'heading') { + level = Math.min(level, token.depth); + } + } + content = renderMarkdownAsPlaintext({ value: content }); + } + + // find first none empty line or use default text + const lineMatch = content.match(/^.*\w+.*\w*$/m); + let preview: string; + if (!lineMatch) { + preview = localize('empty', "empty cell"); + } else { + preview = lineMatch[0].trim(); + if (preview.length >= 64) { + preview = preview.slice(0, 64) + '…'; + } + } + + const entry = new OutlineEntry(i, level, cell, preview, isMarkdown ? Codicon.markdown : Codicon.code); + entries.push(entry); + if (cell.handle === selected) { + this._activeEntry = entry; + } + + // send an event whenever any of the cells change + this._entriesDisposables.add(cell.model.onDidChangeContent(() => { + this._recomputeState(); + this._onDidChange.fire({}); + })); + } + + // build a tree from the list of entries + if (entries.length > 0) { + let result: OutlineEntry[] = [entries[0]]; + let parentStack: OutlineEntry[] = [entries[0]]; + + for (let i = 1; i < entries.length; i++) { + let entry = entries[i]; + + while (true) { + const len = parentStack.length; + if (len === 0) { + // root node + result.push(entry); + parentStack.push(entry); + break; + + } else { + let parentCandidate = parentStack[len - 1]; + if (parentCandidate.level < entry.level) { + parentCandidate.addChild(entry); + parentStack.push(entry); + break; + } else { + parentStack.pop(); + } + } + } + } + this._entries = result; + } + + // feature: show markers with each cell + const markerServiceListener = new MutableDisposable(); + this._entriesDisposables.add(markerServiceListener); + const updateMarkerUpdater = () => { + const doUpdateMarker = (clear: boolean) => { + for (let entry of this._entries) { + if (clear) { + entry.clearMarkers(); + } else { + entry.updateMarkers(this._markerService); + } + } + }; + if (this._configurationService.getValue(OutlineConfigKeys.problemsEnabled)) { + markerServiceListener.value = this._markerService.onMarkerChanged(e => { + if (e.some(uri => viewModel.viewCells.some(cell => isEqual(cell.uri, uri)))) { + doUpdateMarker(false); + this._onDidChange.fire({}); + } + }); + doUpdateMarker(false); + } else { + markerServiceListener.clear(); + doUpdateMarker(true); + } + }; + updateMarkerUpdater(); + this._entriesDisposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(OutlineConfigKeys.problemsEnabled)) { + updateMarkerUpdater(); + this._onDidChange.fire({}); + } + })); + + this._onDidChange.fire({}); + } + + private _recomputeActive(): void { + let newActive: OutlineEntry | undefined; + const { viewModel } = this._editor; + + if (viewModel) { + const [selected] = viewModel.selectionHandles; + const cell = viewModel.getCellByHandle(selected); + if (cell) { + for (let entry of this._entries) { + newActive = entry.find(cell, []); + if (newActive) { + break; + } + } + } + } + if (newActive !== this._activeEntry) { + this._activeEntry = newActive; + this._onDidChange.fire({ affectOnlyActiveElement: true }); + } + } + + get isEmpty(): boolean { + return this._entries.length === 0; + } + + async reveal(entry: OutlineEntry, options: IEditorOptions, sideBySide: boolean): Promise { + await this._editorService.openEditor({ + resource: entry.cell.uri, + options, + }, sideBySide ? SIDE_GROUP : undefined); + } + + preview(entry: OutlineEntry): IDisposable { + const widget = this._editor.getControl(); + if (!widget) { + return Disposable.None; + } + widget.revealInCenterIfOutsideViewport(entry.cell); + const ids = widget.deltaCellDecorations([], [{ + handle: entry.cell.handle, + options: { className: 'nb-symbolHighlight', outputClassName: 'nb-symbolHighlight' } + }]); + return toDisposable(() => { widget.deltaCellDecorations(ids, []); }); + + } + + captureViewState(): IDisposable { + const widget = this._editor.getControl(); + let viewState = widget?.getEditorViewState(); + return toDisposable(() => { + if (viewState) { + widget?.restoreListViewState(viewState); + } + }); + } +} + +class NotebookOutlineCreator implements IOutlineCreator { + + readonly dispose: () => void; + + constructor( + @IOutlineService outlineService: IOutlineService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + const reg = outlineService.registerOutlineCreator(this); + this.dispose = () => reg.dispose(); + } + + matches(candidate: IEditorPane): candidate is NotebookEditor { + return candidate.getId() === NotebookEditor.ID; + } + + async createOutline(editor: NotebookEditor, target: OutlineTarget): Promise | undefined> { + return this._instantiationService.createInstance(NotebookCellOutline, editor, target); + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(NotebookOutlineCreator, LifecyclePhase.Eventually); + + +Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ + id: 'notebook', + order: 100, + type: 'object', + 'properties': { + 'notebook.outline.showCodeCells': { + type: 'boolean', + default: false, + markdownDescription: localize('outline.showCodeCells', "When enabled notebook outline shows code cells.") + }, + 'notebook.breadcrumbs.showCodeCells': { + type: 'boolean', + default: true, + markdownDescription: localize('breadcrumbs.showCodeCells', "When enabled notebook breadcrumbs contain code cells.") + }, + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts index 2f32225c..d3bc0b2a 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts @@ -8,7 +8,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; -import { INotebookActionContext, NOTEBOOK_ACTIONS_CATEGORY, getActiveNotebookEditor } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { NOTEBOOK_ACTIONS_CATEGORY, getActiveNotebookEditor } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; import { INotebookEditor, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -33,11 +33,33 @@ registerAction2(class extends Action2 { title: { value: nls.localize('notebookActions.selectKernel', "Select Notebook Kernel"), original: 'Select Notebook Kernel' }, precondition: NOTEBOOK_IS_ACTIVE_EDITOR, icon: selectKernelIcon, - f1: true + f1: true, + description: { + description: nls.localize('notebookActions.selectKernel.args', "Notebook Kernel Args"), + args: [ + { + name: 'kernelInfo', + description: 'The kernel info', + schema: { + 'type': 'object', + 'required': ['id', 'extension'], + 'properties': { + 'id': { + 'type': 'string' + }, + 'extension': { + 'type': 'string' + } + } + } + } + ] + }, + }); } - async run(accessor: ServicesAccessor, context?: INotebookActionContext): Promise { + async run(accessor: ServicesAccessor, context?: { id: string, extension: string }): Promise { const editorService = accessor.get(IEditorService); const quickInputService = accessor.get(IQuickInputService); const configurationService = accessor.get(IConfigurationService); @@ -52,20 +74,38 @@ registerAction2(class extends Action2 { const picker = quickInputService.createQuickPick<(IQuickPickItem & { run(): void; kernelProviderId?: string })>(); picker.placeholder = nls.localize('notebook.runCell.selectKernel', "Select a notebook kernel to run this notebook"); picker.matchOnDetail = true; - picker.show(); + + + if (context && context.id) { + } else { + picker.show(); + } + picker.busy = true; const tokenSource = new CancellationTokenSource(); - const availableKernels2 = await editor.beginComputeContributedKernels(); - const picks: QuickPickInput[] = [...availableKernels2].map((a) => { + const availableKernels = await editor.beginComputeContributedKernels(); + + const selectedKernel = availableKernels.length ? availableKernels.find( + kernel => kernel.id && context?.id && kernel.id === context?.id && kernel.extension.value === context?.extension + ) : undefined; + + if (selectedKernel) { + editor.activeKernel = selectedKernel!; + return selectedKernel!.resolve(editor.uri!, editor.getId(), tokenSource.token); + } else { + picker.show(); + } + + const picks: QuickPickInput[] = [...availableKernels].map((a) => { return { - id: a.id, + id: a.friendlyId, label: a.label, - picked: a.id === activeKernel?.id, + picked: a.friendlyId === activeKernel?.friendlyId, description: a.description ? a.description - : a.extension.value + (a.id === activeKernel?.id + : a.extension.value + (a.friendlyId === activeKernel?.friendlyId ? nls.localize('currentActiveKernel', " (Currently Active)") : ''), detail: a.detail, diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts b/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts deleted file mode 100644 index 48599557..00000000 --- a/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts +++ /dev/null @@ -1,72 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { TableOfContentsProviderRegistry, ITableOfContentsProvider, ITableOfContentsEntry } from 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess'; -import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; -import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { Codicon } from 'vs/base/common/codicons'; -import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; - -TableOfContentsProviderRegistry.register(NotebookEditor.ID, new class implements ITableOfContentsProvider { - async provideTableOfContents(editor: NotebookEditor, context: { disposables: DisposableStore }) { - if (!editor.viewModel) { - return undefined; - } - // return an entry per markdown header - const notebookWidget = editor.getControl(); - if (!notebookWidget) { - return undefined; - } - - // restore initial view state when no item was picked - let didPickOne = false; - const viewState = notebookWidget.getEditorViewState(); - context.disposables.add(toDisposable(() => { - if (!didPickOne) { - notebookWidget.restoreListViewState(viewState); - } - })); - - let lastDecorationId: string[] = []; - const result: ITableOfContentsEntry[] = []; - for (const cell of editor.viewModel.viewCells) { - const content = cell.getText(); - const regexp = cell.cellKind === CellKind.Markdown - ? /^[ \t]*(\#+)(.+)$/gm // md: header - : /^.*\w+.*\w*$/m; // code: none empty line - - const matches = content.match(regexp); - if (matches && matches.length) { - for (let j = 0; j < matches.length; j++) { - result.push({ - icon: cell.cellKind === CellKind.Markdown ? Codicon.markdown : Codicon.code, - label: matches[j].replace(/^[ \t]*(\#+)/, ''), - pick() { - didPickOne = true; - notebookWidget.revealInCenterIfOutsideViewport(cell); - notebookWidget.selectElement(cell); - notebookWidget.focusNotebookCell(cell, cell.cellKind === CellKind.Markdown ? 'container' : 'editor'); - lastDecorationId = notebookWidget.deltaCellDecorations(lastDecorationId, []); - }, - preview() { - notebookWidget.revealInCenterIfOutsideViewport(cell); - notebookWidget.selectElement(cell); - lastDecorationId = notebookWidget.deltaCellDecorations(lastDecorationId, [{ - handle: cell.handle, - options: { className: 'nb-symbolHighlight', outputClassName: 'nb-symbolHighlight' } - }]); - } - }); - } - } - } - - context.disposables.add(toDisposable(() => { - notebookWidget.deltaCellDecorations(lastDecorationId, []); - })); - - return result; - } -}); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts deleted file mode 100644 index f526080b..00000000 --- a/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts +++ /dev/null @@ -1,1091 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as DOM from 'vs/base/browser/dom'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { CellDiffViewModel, PropertyFoldingState } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; -import { CellDiffRenderTemplate, CellDiffViewModelLayoutChangeEvent, DIFF_CELL_MARGIN, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/common'; -import { EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { format } from 'vs/base/common/jsonFormatter'; -import { applyEdits } from 'vs/base/common/jsonEdit'; -import { CellEditType, CellUri, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { hash } from 'vs/base/common/hash'; -import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IMenu, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IAction } from 'vs/base/common/actions'; -import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellActionView'; -import { getEditorTopPadding } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { collapsedIcon, expandedIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; -import { renderCodicons } from 'vs/base/browser/codicons'; - -const fixedEditorOptions: IEditorOptions = { - padding: { - top: 12, - bottom: 12 - }, - scrollBeyondLastLine: false, - scrollbar: { - verticalScrollbarSize: 14, - horizontal: 'auto', - useShadows: true, - verticalHasArrows: false, - horizontalHasArrows: false, - alwaysConsumeMouseWheel: false - }, - renderLineHighlightOnlyWhenFocus: true, - overviewRulerLanes: 0, - selectOnLineNumbers: false, - wordWrap: 'off', - lineNumbers: 'off', - lineDecorationsWidth: 0, - glyphMargin: false, - fixedOverflowWidgets: true, - minimap: { enabled: false }, - renderValidationDecorations: 'on', - renderLineHighlight: 'none', - readOnly: true -}; - -const fixedDiffEditorOptions: IDiffEditorOptions = { - ...fixedEditorOptions, - glyphMargin: true, - enableSplitViewResizing: false, - renderIndicators: false, - readOnly: false, - isInEmbeddedEditor: true -}; - - - -class PropertyHeader extends Disposable { - protected _foldingIndicator!: HTMLElement; - protected _statusSpan!: HTMLElement; - protected _toolbar!: ToolBar; - protected _menu!: IMenu; - - constructor( - readonly cell: CellDiffViewModel, - readonly metadataHeaderContainer: HTMLElement, - readonly notebookEditor: INotebookTextDiffEditor, - readonly accessor: { - updateInfoRendering: () => void; - checkIfModified: (cell: CellDiffViewModel) => boolean; - getFoldingState: (cell: CellDiffViewModel) => PropertyFoldingState; - updateFoldingState: (cell: CellDiffViewModel, newState: PropertyFoldingState) => void; - unChangedLabel: string; - changedLabel: string; - prefix: string; - menuId: MenuId; - }, - @IContextMenuService readonly contextMenuService: IContextMenuService, - @IKeybindingService readonly keybindingService: IKeybindingService, - @INotificationService readonly notificationService: INotificationService, - @IMenuService readonly menuService: IMenuService, - @IContextKeyService readonly contextKeyService: IContextKeyService - ) { - super(); - } - - buildHeader(): void { - let metadataChanged = this.accessor.checkIfModified(this.cell); - this._foldingIndicator = DOM.append(this.metadataHeaderContainer, DOM.$('.property-folding-indicator')); - this._foldingIndicator.classList.add(this.accessor.prefix); - this._updateFoldingIcon(); - const metadataStatus = DOM.append(this.metadataHeaderContainer, DOM.$('div.property-status')); - this._statusSpan = DOM.append(metadataStatus, DOM.$('span')); - - if (metadataChanged) { - this._statusSpan.textContent = this.accessor.changedLabel; - this._statusSpan.style.fontWeight = 'bold'; - this.metadataHeaderContainer.classList.add('modified'); - } else { - this._statusSpan.textContent = this.accessor.unChangedLabel; - } - - const cellToolbarContainer = DOM.append(this.metadataHeaderContainer, DOM.$('div.property-toolbar')); - this._toolbar = new ToolBar(cellToolbarContainer, this.contextMenuService, { - actionViewItemProvider: action => { - if (action instanceof MenuItemAction) { - const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService); - return item; - } - - return undefined; - } - }); - this._register(this._toolbar); - this._toolbar.context = { - cell: this.cell - }; - - this._menu = this.menuService.createMenu(this.accessor.menuId, this.contextKeyService); - this._register(this._menu); - - if (metadataChanged) { - const actions: IAction[] = []; - createAndFillInActionBarActions(this._menu, { shouldForwardArgs: true }, actions); - this._toolbar.setActions(actions); - } - - this._register(this.notebookEditor.onMouseUp(e => { - if (!e.event.target) { - return; - } - - const target = e.event.target as HTMLElement; - - if (target.classList.contains('codicon-notebook-collapsed') || target.classList.contains('codicon-notebook-expanded')) { - const parent = target.parentElement as HTMLElement; - - if (!parent) { - return; - } - - if (!parent.classList.contains(this.accessor.prefix)) { - return; - } - - if (!parent.classList.contains('property-folding-indicator')) { - return; - } - - // folding icon - - const cellViewModel = e.target; - - if (cellViewModel === this.cell) { - const oldFoldingState = this.accessor.getFoldingState(this.cell); - this.accessor.updateFoldingState(this.cell, oldFoldingState === PropertyFoldingState.Expanded ? PropertyFoldingState.Collapsed : PropertyFoldingState.Expanded); - this._updateFoldingIcon(); - this.accessor.updateInfoRendering(); - } - } - - return; - })); - - this._updateFoldingIcon(); - this.accessor.updateInfoRendering(); - } - - refresh() { - let metadataChanged = this.accessor.checkIfModified(this.cell); - if (metadataChanged) { - this._statusSpan.textContent = this.accessor.changedLabel; - this._statusSpan.style.fontWeight = 'bold'; - this.metadataHeaderContainer.classList.add('modified'); - const actions: IAction[] = []; - createAndFillInActionBarActions(this._menu, undefined, actions); - this._toolbar.setActions(actions); - } else { - this._statusSpan.textContent = this.accessor.unChangedLabel; - this._statusSpan.style.fontWeight = 'normal'; - this._toolbar.setActions([]); - } - } - - private _updateFoldingIcon() { - if (this.accessor.getFoldingState(this.cell) === PropertyFoldingState.Collapsed) { - DOM.reset(this._foldingIndicator, ...renderCodicons(ThemeIcon.asCodiconLabel(collapsedIcon))); - } else { - DOM.reset(this._foldingIndicator, ...renderCodicons(ThemeIcon.asCodiconLabel(expandedIcon))); - } - } -} - -abstract class AbstractCellRenderer extends Disposable { - protected _metadataHeaderContainer!: HTMLElement; - protected _metadataHeader!: PropertyHeader; - protected _metadataInfoContainer!: HTMLElement; - protected _metadataEditorContainer?: HTMLElement; - protected _metadataEditorDisposeStore!: DisposableStore; - protected _metadataEditor?: CodeEditorWidget | DiffEditorWidget; - - protected _outputHeaderContainer!: HTMLElement; - protected _outputHeader!: PropertyHeader; - protected _outputInfoContainer!: HTMLElement; - protected _outputEditorContainer?: HTMLElement; - protected _outputEditorDisposeStore!: DisposableStore; - protected _outputEditor?: CodeEditorWidget | DiffEditorWidget; - - - protected _diffEditorContainer!: HTMLElement; - protected _diagonalFill?: HTMLElement; - protected _layoutInfo!: { - editorHeight: number; - editorMargin: number; - metadataStatusHeight: number; - metadataHeight: number; - outputStatusHeight: number; - outputHeight: number; - bodyMargin: number; - }; - protected _isDisposed: boolean; - - constructor( - readonly notebookEditor: INotebookTextDiffEditor, - readonly cell: CellDiffViewModel, - readonly templateData: CellDiffRenderTemplate, - readonly style: 'left' | 'right' | 'full', - protected readonly instantiationService: IInstantiationService, - protected readonly modeService: IModeService, - protected readonly modelService: IModelService, - protected readonly contextMenuService: IContextMenuService, - protected readonly keybindingService: IKeybindingService, - protected readonly notificationService: INotificationService, - protected readonly menuService: IMenuService, - protected readonly contextKeyService: IContextKeyService - - - ) { - super(); - // init - this._isDisposed = false; - this._layoutInfo = { - editorHeight: 0, - editorMargin: 0, - metadataHeight: 0, - metadataStatusHeight: 25, - outputHeight: 0, - outputStatusHeight: 25, - bodyMargin: 32 - }; - this._metadataEditorDisposeStore = new DisposableStore(); - this._outputEditorDisposeStore = new DisposableStore(); - this._register(this._metadataEditorDisposeStore); - this.initData(); - this.buildBody(templateData.container); - this._register(cell.onDidLayoutChange(e => this.onDidLayoutChange(e))); - } - - buildBody(container: HTMLElement) { - const body = DOM.$('.cell-body'); - DOM.append(container, body); - this._diffEditorContainer = DOM.$('.cell-diff-editor-container'); - switch (this.style) { - case 'left': - body.classList.add('left'); - break; - case 'right': - body.classList.add('right'); - break; - default: - body.classList.add('full'); - break; - } - - DOM.append(body, this._diffEditorContainer); - this._diagonalFill = DOM.append(body, DOM.$('.diagonal-fill')); - this.styleContainer(this._diffEditorContainer); - const sourceContainer = DOM.append(this._diffEditorContainer, DOM.$('.source-container')); - this.buildSourceEditor(sourceContainer); - - this._metadataHeaderContainer = DOM.append(this._diffEditorContainer, DOM.$('.metadata-header-container')); - this._metadataInfoContainer = DOM.append(this._diffEditorContainer, DOM.$('.metadata-info-container')); - - const checkIfModified = (cell: CellDiffViewModel) => { - return cell.type !== 'delete' && cell.type !== 'insert' && hash(this._getFormatedMetadataJSON(cell.original?.metadata || {}, cell.original?.language)) !== hash(this._getFormatedMetadataJSON(cell.modified?.metadata ?? {}, cell.modified?.language)); - }; - - if (checkIfModified(this.cell)) { - this.cell.metadataFoldingState = PropertyFoldingState.Expanded; - } - - this._metadataHeader = this.instantiationService.createInstance( - PropertyHeader, - this.cell, - this._metadataHeaderContainer, - this.notebookEditor, - { - updateInfoRendering: this.updateMetadataRendering.bind(this), - checkIfModified: (cell) => { - return checkIfModified(cell); - }, - getFoldingState: (cell) => { - return cell.metadataFoldingState; - }, - updateFoldingState: (cell, state) => { - cell.metadataFoldingState = state; - }, - unChangedLabel: 'Metadata', - changedLabel: 'Metadata changed', - prefix: 'metadata', - menuId: MenuId.NotebookDiffCellMetadataTitle - } - ); - this._register(this._metadataHeader); - this._metadataHeader.buildHeader(); - - if (this.notebookEditor.textModel?.transientOptions.transientOutputs) { - this._layoutInfo.outputHeight = 0; - this._layoutInfo.outputStatusHeight = 0; - this.layout({}); - return; - } - - this._outputHeaderContainer = DOM.append(this._diffEditorContainer, DOM.$('.output-header-container')); - this._outputInfoContainer = DOM.append(this._diffEditorContainer, DOM.$('.output-info-container')); - - const checkIfOutputsModified = (cell: CellDiffViewModel) => { - return cell.type !== 'delete' && cell.type !== 'insert' && !this.notebookEditor.textModel!.transientOptions.transientOutputs && cell.type === 'modified' && hash(cell.original?.outputs ?? []) !== hash(cell.modified?.outputs ?? []); - }; - - if (checkIfOutputsModified(this.cell)) { - this.cell.outputFoldingState = PropertyFoldingState.Expanded; - } - - this._outputHeader = this.instantiationService.createInstance( - PropertyHeader, - this.cell, - this._outputHeaderContainer, - this.notebookEditor, - { - updateInfoRendering: this.updateOutputRendering.bind(this), - checkIfModified: (cell) => { - return checkIfOutputsModified(cell); - }, - getFoldingState: (cell) => { - return cell.outputFoldingState; - }, - updateFoldingState: (cell, state) => { - cell.outputFoldingState = state; - }, - unChangedLabel: 'Outputs', - changedLabel: 'Outputs changed', - prefix: 'output', - menuId: MenuId.NotebookDiffCellOutputsTitle - } - ); - this._register(this._outputHeader); - this._outputHeader.buildHeader(); - } - - updateMetadataRendering() { - if (this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { - // we should expand the metadata editor - this._metadataInfoContainer.style.display = 'block'; - - if (!this._metadataEditorContainer || !this._metadataEditor) { - // create editor - this._metadataEditorContainer = DOM.append(this._metadataInfoContainer, DOM.$('.metadata-editor-container')); - this._buildMetadataEditor(); - } else { - this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight(); - this.layout({ metadataEditor: true }); - } - } else { - // we should collapse the metadata editor - this._metadataInfoContainer.style.display = 'none'; - this._metadataEditorDisposeStore.clear(); - this._layoutInfo.metadataHeight = 0; - this.layout({}); - } - } - - updateOutputRendering() { - if (this.cell.outputFoldingState === PropertyFoldingState.Expanded) { - this._outputInfoContainer.style.display = 'block'; - - if (!this._outputEditorContainer || !this._outputEditor) { - // create editor - this._outputEditorContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-editor-container')); - this._buildOutputEditor(); - } else { - this._layoutInfo.outputHeight = this._outputEditor.getContentHeight(); - this.layout({ outputEditor: true }); - } - } else { - this._outputInfoContainer.style.display = 'none'; - this._outputEditorDisposeStore.clear(); - this._layoutInfo.outputHeight = 0; - this.layout({}); - } - } - - protected _getFormatedMetadataJSON(metadata: NotebookCellMetadata, language?: string) { - let filteredMetadata: { [key: string]: any } = {}; - - if (this.notebookEditor.textModel) { - const transientMetadata = this.notebookEditor.textModel!.transientOptions.transientMetadata; - - const keys = new Set([...Object.keys(metadata)]); - for (let key of keys) { - if (!(transientMetadata[key as keyof NotebookCellMetadata]) - ) { - filteredMetadata[key] = metadata[key as keyof NotebookCellMetadata]; - } - } - } else { - filteredMetadata = metadata; - } - - const content = JSON.stringify({ - language, - ...filteredMetadata - }); - - const edits = format(content, undefined, {}); - const metadataSource = applyEdits(content, edits); - - return metadataSource; - } - - private _applySanitizedMetadataChanges(currentMetadata: NotebookCellMetadata, newMetadata: any) { - let result: { [key: string]: any } = {}; - let newLangauge: string | undefined = undefined; - try { - const newMetadataObj = JSON.parse(newMetadata); - const keys = new Set([...Object.keys(newMetadataObj)]); - for (let key of keys) { - switch (key as keyof NotebookCellMetadata) { - case 'breakpointMargin': - case 'editable': - case 'hasExecutionOrder': - case 'inputCollapsed': - case 'outputCollapsed': - case 'runnable': - // boolean - if (typeof newMetadataObj[key] === 'boolean') { - result[key] = newMetadataObj[key]; - } else { - result[key] = currentMetadata[key as keyof NotebookCellMetadata]; - } - break; - - case 'executionOrder': - case 'lastRunDuration': - // number - if (typeof newMetadataObj[key] === 'number') { - result[key] = newMetadataObj[key]; - } else { - result[key] = currentMetadata[key as keyof NotebookCellMetadata]; - } - break; - case 'runState': - // enum - if (typeof newMetadataObj[key] === 'number' && [1, 2, 3, 4].indexOf(newMetadataObj[key]) >= 0) { - result[key] = newMetadataObj[key]; - } else { - result[key] = currentMetadata[key as keyof NotebookCellMetadata]; - } - break; - case 'statusMessage': - // string - if (typeof newMetadataObj[key] === 'string') { - result[key] = newMetadataObj[key]; - } else { - result[key] = currentMetadata[key as keyof NotebookCellMetadata]; - } - break; - default: - if (key === 'language') { - newLangauge = newMetadataObj[key]; - } - result[key] = newMetadataObj[key]; - break; - } - } - - if (newLangauge !== undefined && newLangauge !== this.cell.modified!.language) { - const index = this.notebookEditor.textModel!.cells.indexOf(this.cell.modified!); - this.notebookEditor.textModel!.applyEdits( - this.notebookEditor.textModel!.versionId, - [{ editType: CellEditType.CellLanguage, index, language: newLangauge }], - true, - undefined, - () => undefined, - undefined - ); - } - - const index = this.notebookEditor.textModel!.cells.indexOf(this.cell.modified!); - - if (index < 0) { - return; - } - - this.notebookEditor.textModel!.applyEdits(this.notebookEditor.textModel!.versionId, [ - { editType: CellEditType.Metadata, index, metadata: result } - ], true, undefined, () => undefined, undefined); - } catch { - } - } - - private _buildMetadataEditor() { - if (this.cell.type === 'modified' || this.cell.type === 'unchanged') { - const originalMetadataSource = this._getFormatedMetadataJSON(this.cell.original?.metadata || {}, this.cell.original?.language); - const modifiedMetadataSource = this._getFormatedMetadataJSON(this.cell.modified?.metadata || {}, this.cell.modified?.language); - this._metadataEditor = this.instantiationService.createInstance(DiffEditorWidget, this._metadataEditorContainer!, { - ...fixedDiffEditorOptions, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), - readOnly: false, - originalEditable: false, - ignoreTrimWhitespace: false - }); - this._register(this._metadataEditor); - - this._metadataEditorContainer?.classList.add('diff'); - - const mode = this.modeService.create('json'); - const originalMetadataModel = this.modelService.createModel(originalMetadataSource, mode, CellUri.generateCellMetadataUri(this.cell.original!.uri, this.cell.original!.handle), false); - const modifiedMetadataModel = this.modelService.createModel(modifiedMetadataSource, mode, CellUri.generateCellMetadataUri(this.cell.modified!.uri, this.cell.modified!.handle), false); - this._metadataEditor.setModel({ - original: originalMetadataModel, - modified: modifiedMetadataModel - }); - - this._register(originalMetadataModel); - this._register(modifiedMetadataModel); - - this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight(); - this.layout({ metadataEditor: true }); - - this._register(this._metadataEditor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { - this._layoutInfo.metadataHeight = e.contentHeight; - this.layout({ metadataEditor: true }); - } - })); - - let respondingToContentChange = false; - - this._register(modifiedMetadataModel.onDidChangeContent(() => { - respondingToContentChange = true; - const value = modifiedMetadataModel.getValue(); - this._applySanitizedMetadataChanges(this.cell.modified!.metadata, value); - this._metadataHeader.refresh(); - respondingToContentChange = false; - })); - - this._register(this.cell.modified!.onDidChangeMetadata(() => { - if (respondingToContentChange) { - return; - } - - const modifiedMetadataSource = this._getFormatedMetadataJSON(this.cell.modified?.metadata || {}, this.cell.modified?.language); - modifiedMetadataModel.setValue(modifiedMetadataSource); - })); - - return; - } - - this._metadataEditor = this.instantiationService.createInstance(CodeEditorWidget, this._metadataEditorContainer!, { - ...fixedEditorOptions, - dimension: { - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), - height: 0 - }, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), - readOnly: false - }, {}); - this._register(this._metadataEditor); - - const mode = this.modeService.create('jsonc'); - const originalMetadataSource = this._getFormatedMetadataJSON( - this.cell.type === 'insert' - ? this.cell.modified!.metadata || {} - : this.cell.original!.metadata || {}); - const uri = this.cell.type === 'insert' - ? this.cell.modified!.uri - : this.cell.original!.uri; - const handle = this.cell.type === 'insert' - ? this.cell.modified!.handle - : this.cell.original!.handle; - - const modelUri = CellUri.generateCellMetadataUri(uri, handle); - const metadataModel = this.modelService.createModel(originalMetadataSource, mode, modelUri, false); - this._metadataEditor.setModel(metadataModel); - this._register(metadataModel); - - this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight(); - this.layout({ metadataEditor: true }); - - this._register(this._metadataEditor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { - this._layoutInfo.metadataHeight = e.contentHeight; - this.layout({ metadataEditor: true }); - } - })); - } - - private _getFormatedOutputJSON(outputs: any[]) { - const content = JSON.stringify(outputs); - - const edits = format(content, undefined, {}); - const source = applyEdits(content, edits); - - return source; - } - - private _buildOutputEditor() { - if ((this.cell.type === 'modified' || this.cell.type === 'unchanged') && !this.notebookEditor.textModel!.transientOptions.transientOutputs) { - const originalOutputsSource = this._getFormatedOutputJSON(this.cell.original?.outputs || []); - const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []); - if (originalOutputsSource !== modifiedOutputsSource) { - this._outputEditor = this.instantiationService.createInstance(DiffEditorWidget, this._outputEditorContainer!, { - ...fixedDiffEditorOptions, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), - readOnly: true, - ignoreTrimWhitespace: false - }); - this._register(this._outputEditor); - - this._outputEditorContainer?.classList.add('diff'); - - const mode = this.modeService.create('json'); - const originalModel = this.modelService.createModel(originalOutputsSource, mode, undefined, true); - const modifiedModel = this.modelService.createModel(modifiedOutputsSource, mode, undefined, true); - this._outputEditor.setModel({ - original: originalModel, - modified: modifiedModel - }); - - this._layoutInfo.outputHeight = this._outputEditor.getContentHeight(); - this.layout({ outputEditor: true }); - - this._register(this._outputEditor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) { - this._layoutInfo.outputHeight = e.contentHeight; - this.layout({ outputEditor: true }); - } - })); - - this._register(this.cell.modified!.onDidChangeOutputs(() => { - const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []); - modifiedModel.setValue(modifiedOutputsSource); - this._outputHeader.refresh(); - })); - - return; - } - } - - this._outputEditor = this.instantiationService.createInstance(CodeEditorWidget, this._outputEditorContainer!, { - ...fixedEditorOptions, - dimension: { - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), - height: 0 - }, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() - }, {}); - this._register(this._outputEditor); - - const mode = this.modeService.create('json'); - const originaloutputSource = this._getFormatedOutputJSON( - this.notebookEditor.textModel!.transientOptions.transientOutputs - ? [] - : this.cell.type === 'insert' - ? this.cell.modified!.outputs || [] - : this.cell.original!.outputs || []); - const outputModel = this.modelService.createModel(originaloutputSource, mode, undefined, true); - this._outputEditor.setModel(outputModel); - - this._layoutInfo.outputHeight = this._outputEditor.getContentHeight(); - this.layout({ outputEditor: true }); - - this._register(this._outputEditor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) { - this._layoutInfo.outputHeight = e.contentHeight; - this.layout({ outputEditor: true }); - } - })); - } - - protected layoutNotebookCell() { - this.notebookEditor.layoutNotebookCell( - this.cell, - this._layoutInfo.editorHeight - + this._layoutInfo.editorMargin - + this._layoutInfo.metadataHeight - + this._layoutInfo.metadataStatusHeight - + this._layoutInfo.outputHeight - + this._layoutInfo.outputStatusHeight - + this._layoutInfo.bodyMargin - ); - } - - dispose() { - this._isDisposed = true; - super.dispose(); - } - - abstract initData(): void; - abstract styleContainer(container: HTMLElement): void; - abstract buildSourceEditor(sourceContainer: HTMLElement): void; - abstract onDidLayoutChange(event: CellDiffViewModelLayoutChangeEvent): void; - abstract layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }): void; -} - -export class DeletedCell extends AbstractCellRenderer { - private _editor!: CodeEditorWidget; - constructor( - readonly notebookEditor: INotebookTextDiffEditor, - readonly cell: CellDiffViewModel, - readonly templateData: CellDiffRenderTemplate, - @IModeService readonly modeService: IModeService, - @IModelService readonly modelService: IModelService, - @IInstantiationService protected readonly instantiationService: IInstantiationService, - @IContextMenuService protected readonly contextMenuService: IContextMenuService, - @IKeybindingService protected readonly keybindingService: IKeybindingService, - @INotificationService protected readonly notificationService: INotificationService, - @IMenuService protected readonly menuService: IMenuService, - @IContextKeyService protected readonly contextKeyService: IContextKeyService, - - - ) { - super(notebookEditor, cell, templateData, 'left', instantiationService, modeService, modelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService); - } - - initData(): void { - } - - styleContainer(container: HTMLElement) { - container.classList.add('removed'); - } - - buildSourceEditor(sourceContainer: HTMLElement): void { - const originalCell = this.cell.original!; - const lineCount = originalCell.textBuffer.getLineCount(); - const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; - const editorHeight = lineCount * lineHeight + getEditorTopPadding() + EDITOR_BOTTOM_PADDING; - - const editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container')); - - this._editor = this.instantiationService.createInstance(CodeEditorWidget, editorContainer, { - ...fixedEditorOptions, - dimension: { - width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18, - height: editorHeight - }, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() - }, {}); - this._register(this._editor); - - this._layoutInfo.editorHeight = editorHeight; - - this._register(this._editor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this._layoutInfo.editorHeight !== e.contentHeight) { - this._layoutInfo.editorHeight = e.contentHeight; - this.layout({ editorHeight: true }); - } - })); - - originalCell.resolveTextModelRef().then(ref => { - if (this._isDisposed) { - return; - } - - this._register(ref); - - const textModel = ref.object.textEditorModel; - this._editor.setModel(textModel); - this._layoutInfo.editorHeight = this._editor.getContentHeight(); - this.layout({ editorHeight: true }); - }); - - } - - onDidLayoutChange(e: CellDiffViewModelLayoutChangeEvent) { - if (e.outerWidth !== undefined) { - this.layout({ outerWidth: true }); - } - } - layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }) { - DOM.scheduleAtNextAnimationFrame(() => { - if (state.editorHeight || state.outerWidth) { - this._editor.layout({ - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), - height: this._layoutInfo.editorHeight - }); - } - - if (state.metadataEditor || state.outerWidth) { - this._metadataEditor?.layout({ - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), - height: this._layoutInfo.metadataHeight - }); - } - - if (state.outputEditor || state.outerWidth) { - this._outputEditor?.layout({ - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), - height: this._layoutInfo.outputHeight - }); - } - - this.layoutNotebookCell(); - }); - } -} - -export class InsertCell extends AbstractCellRenderer { - private _editor!: CodeEditorWidget; - constructor( - readonly notebookEditor: INotebookTextDiffEditor, - readonly cell: CellDiffViewModel, - readonly templateData: CellDiffRenderTemplate, - @IInstantiationService protected readonly instantiationService: IInstantiationService, - @IModeService readonly modeService: IModeService, - @IModelService readonly modelService: IModelService, - @IContextMenuService protected readonly contextMenuService: IContextMenuService, - @IKeybindingService protected readonly keybindingService: IKeybindingService, - @INotificationService protected readonly notificationService: INotificationService, - @IMenuService protected readonly menuService: IMenuService, - @IContextKeyService protected readonly contextKeyService: IContextKeyService, - ) { - super(notebookEditor, cell, templateData, 'right', instantiationService, modeService, modelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService); - } - - initData(): void { - } - - styleContainer(container: HTMLElement): void { - container.classList.add('inserted'); - } - - buildSourceEditor(sourceContainer: HTMLElement): void { - const modifiedCell = this.cell.modified!; - const lineCount = modifiedCell.textBuffer.getLineCount(); - const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; - const editorHeight = lineCount * lineHeight + getEditorTopPadding() + EDITOR_BOTTOM_PADDING; - const editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container')); - - this._editor = this.instantiationService.createInstance(CodeEditorWidget, editorContainer, { - ...fixedEditorOptions, - dimension: { - width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18, - height: editorHeight - }, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), - readOnly: false - }, {}); - this._register(this._editor); - - this._layoutInfo.editorHeight = editorHeight; - - this._register(this._editor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this._layoutInfo.editorHeight !== e.contentHeight) { - this._layoutInfo.editorHeight = e.contentHeight; - this.layout({ editorHeight: true }); - } - })); - - modifiedCell.resolveTextModelRef().then(ref => { - if (this._isDisposed) { - return; - } - - this._register(ref); - - const textModel = ref.object.textEditorModel; - this._editor.setModel(textModel); - this._layoutInfo.editorHeight = this._editor.getContentHeight(); - this.layout({ editorHeight: true }); - }); - } - - onDidLayoutChange(e: CellDiffViewModelLayoutChangeEvent) { - if (e.outerWidth !== undefined) { - this.layout({ outerWidth: true }); - } - } - - layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }) { - DOM.scheduleAtNextAnimationFrame(() => { - if (state.editorHeight || state.outerWidth) { - this._editor.layout({ - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), - height: this._layoutInfo.editorHeight - }); - } - - if (state.metadataEditor || state.outerWidth) { - this._metadataEditor?.layout({ - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), - height: this._layoutInfo.metadataHeight - }); - } - - if (state.outputEditor || state.outerWidth) { - this._outputEditor?.layout({ - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), - height: this._layoutInfo.outputHeight - }); - } - - this.layoutNotebookCell(); - }); - } -} - -export class ModifiedCell extends AbstractCellRenderer { - private _editor?: DiffEditorWidget; - private _editorContainer!: HTMLElement; - private _inputToolbarContainer!: HTMLElement; - protected _toolbar!: ToolBar; - protected _menu!: IMenu; - - constructor( - readonly notebookEditor: INotebookTextDiffEditor, - readonly cell: CellDiffViewModel, - readonly templateData: CellDiffRenderTemplate, - @IInstantiationService protected readonly instantiationService: IInstantiationService, - @IModeService readonly modeService: IModeService, - @IModelService readonly modelService: IModelService, - @IContextMenuService protected readonly contextMenuService: IContextMenuService, - @IKeybindingService protected readonly keybindingService: IKeybindingService, - @INotificationService protected readonly notificationService: INotificationService, - @IMenuService protected readonly menuService: IMenuService, - @IContextKeyService protected readonly contextKeyService: IContextKeyService - ) { - super(notebookEditor, cell, templateData, 'full', instantiationService, modeService, modelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService); - } - - initData(): void { - } - - styleContainer(container: HTMLElement): void { - } - - buildSourceEditor(sourceContainer: HTMLElement): void { - const modifiedCell = this.cell.modified!; - const lineCount = modifiedCell.textBuffer.getLineCount(); - const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; - const editorHeight = lineCount * lineHeight + getEditorTopPadding() + EDITOR_BOTTOM_PADDING; - this._editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container')); - - this._editor = this.instantiationService.createInstance(DiffEditorWidget, this._editorContainer, { - ...fixedDiffEditorOptions, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), - originalEditable: false, - ignoreTrimWhitespace: false - }); - this._register(this._editor); - this._editorContainer.classList.add('diff'); - - this._editor.layout({ - width: this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN, - height: editorHeight - }); - - this._editorContainer.style.height = `${editorHeight}px`; - - this._register(this._editor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this._layoutInfo.editorHeight !== e.contentHeight) { - this._layoutInfo.editorHeight = e.contentHeight; - this.layout({ editorHeight: true }); - } - })); - - this._initializeSourceDiffEditor(); - - this._inputToolbarContainer = DOM.append(sourceContainer, DOM.$('.editor-input-toolbar-container')); - const cellToolbarContainer = DOM.append(this._inputToolbarContainer, DOM.$('div.property-toolbar')); - this._toolbar = new ToolBar(cellToolbarContainer, this.contextMenuService, { - actionViewItemProvider: action => { - if (action instanceof MenuItemAction) { - const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService); - return item; - } - - return undefined; - } - }); - - this._toolbar.context = { - cell: this.cell - }; - - this._menu = this.menuService.createMenu(MenuId.NotebookDiffCellInputTitle, this.contextKeyService); - this._register(this._menu); - const actions: IAction[] = []; - createAndFillInActionBarActions(this._menu, { shouldForwardArgs: true }, actions); - this._toolbar.setActions(actions); - - if (this.cell.modified!.getValue() !== this.cell.original!.getValue()) { - this._inputToolbarContainer.style.display = 'block'; - } else { - this._inputToolbarContainer.style.display = 'none'; - } - - this._register(this.cell.modified!.onDidChangeContent(() => { - if (this.cell.modified!.getValue() !== this.cell.original!.getValue()) { - this._inputToolbarContainer.style.display = 'block'; - } else { - this._inputToolbarContainer.style.display = 'none'; - } - })); - } - - private async _initializeSourceDiffEditor() { - const originalCell = this.cell.original!; - const modifiedCell = this.cell.modified!; - - const originalRef = await originalCell.resolveTextModelRef(); - const modifiedRef = await modifiedCell.resolveTextModelRef(); - - if (this._isDisposed) { - return; - } - - const textModel = originalRef.object.textEditorModel; - const modifiedTextModel = modifiedRef.object.textEditorModel; - this._register(originalRef); - this._register(modifiedRef); - - this._editor!.setModel({ - original: textModel, - modified: modifiedTextModel - }); - - const contentHeight = this._editor!.getContentHeight(); - this._layoutInfo.editorHeight = contentHeight; - this.layout({ editorHeight: true }); - - } - - onDidLayoutChange(e: CellDiffViewModelLayoutChangeEvent) { - if (e.outerWidth !== undefined) { - this.layout({ outerWidth: true }); - } - } - - layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }) { - DOM.scheduleAtNextAnimationFrame(() => { - if (state.editorHeight || state.outerWidth) { - this._editorContainer.style.height = `${this._layoutInfo.editorHeight}px`; - this._editor!.layout(); - } - - if (state.metadataEditor || state.outerWidth) { - if (this._metadataEditorContainer) { - this._metadataEditorContainer.style.height = `${this._layoutInfo.metadataHeight}px`; - this._metadataEditor?.layout(); - } - } - - if (state.outputEditor || state.outerWidth) { - if (this._outputEditorContainer) { - this._outputEditorContainer.style.height = `${this._layoutInfo.outputHeight}px`; - this._outputEditor?.layout(); - } - } - - this.layoutNotebookCell(); - }); - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel.ts deleted file mode 100644 index 55e25cee..00000000 --- a/src/vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel.ts +++ /dev/null @@ -1,48 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { NotebookDiffEditorEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; -import { Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { CellDiffViewModelLayoutChangeEvent, DIFF_CELL_MARGIN } from 'vs/workbench/contrib/notebook/browser/diff/common'; -import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; - -export enum PropertyFoldingState { - Expanded, - Collapsed -} - -export class CellDiffViewModel extends Disposable { - public metadataFoldingState: PropertyFoldingState; - public outputFoldingState: PropertyFoldingState; - private _layoutInfoEmitter = new Emitter(); - - onDidLayoutChange = this._layoutInfoEmitter.event; - - constructor( - readonly original: NotebookCellTextModel | undefined, - readonly modified: NotebookCellTextModel | undefined, - readonly type: 'unchanged' | 'insert' | 'delete' | 'modified', - readonly editorEventDispatcher: NotebookDiffEditorEventDispatcher - ) { - super(); - this.metadataFoldingState = PropertyFoldingState.Collapsed; - this.outputFoldingState = PropertyFoldingState.Collapsed; - - this._register(this.editorEventDispatcher.onDidChangeLayout(e => { - this._layoutInfoEmitter.fire({ outerWidth: e.value.width }); - })); - } - - getComputedCellContainerWidth(layoutInfo: NotebookLayoutInfo, diffEditor: boolean, fullWidth: boolean) { - if (fullWidth) { - return layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0) - 2; - } - - return (layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)) / 2 - 18 - 2; - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/common.ts b/src/vs/workbench/contrib/notebook/browser/diff/common.ts deleted file mode 100644 index 505718ce..00000000 --- a/src/vs/workbench/contrib/notebook/browser/diff/common.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; -import { Event } from 'vs/base/common/event'; -import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; - -export interface INotebookTextDiffEditor { - readonly textModel?: NotebookTextModel; - onMouseUp: Event<{ readonly event: MouseEvent; readonly target: CellDiffViewModel; }>; - getOverflowContainerDomNode(): HTMLElement; - getLayoutInfo(): NotebookLayoutInfo; - layoutNotebookCell(cell: CellDiffViewModel, height: number): void; -} - -export interface CellDiffRenderTemplate { - readonly container: HTMLElement; - readonly elementDisposables: DisposableStore; -} - -export interface CellDiffViewModelLayoutChangeEvent { - font?: BareFontInfo; - outerWidth?: number; -} - -export const DIFF_CELL_MARGIN = 16; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts new file mode 100644 index 00000000..e2f4b6cd --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -0,0 +1,1463 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { DiffElementViewModelBase, getFormatedMetadataJSON, OUTPUT_EDITOR_HEIGHT_MAGIC, PropertyFoldingState, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { CellDiffSideBySideRenderTemplate, CellDiffSingleSideRenderTemplate, DiffSide, DIFF_CELL_MARGIN, INotebookTextDiffEditor, NOTEBOOK_DIFF_CELL_PROPERTY, NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; +import { EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { CellEditType, CellUri, IProcessedOutput, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IMenu, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IAction } from 'vs/base/common/actions'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { Delayer } from 'vs/base/common/async'; +import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellActionView'; +import { getEditorTopPadding } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { collapsedIcon, expandedIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { OutputContainer } from 'vs/workbench/contrib/notebook/browser/diff/diffElementOutputs'; +import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; +import { ContextMenuController } from 'vs/editor/contrib/contextmenu/contextmenu'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; +import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; +import { AccessibilityHelpController } from 'vs/workbench/contrib/codeEditor/browser/accessibility/accessibility'; +import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer'; +import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; +import { TabCompletionController } from 'vs/workbench/contrib/snippets/browser/tabCompletion'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import * as editorCommon from 'vs/editor/common/editorCommon'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; + +export const fixedEditorOptions: IEditorOptions = { + padding: { + top: 12, + bottom: 12 + }, + scrollBeyondLastLine: false, + scrollbar: { + verticalScrollbarSize: 14, + horizontal: 'auto', + vertical: 'hidden', + useShadows: true, + verticalHasArrows: false, + horizontalHasArrows: false, + alwaysConsumeMouseWheel: false, + }, + renderLineHighlightOnlyWhenFocus: true, + overviewRulerLanes: 0, + overviewRulerBorder: false, + selectOnLineNumbers: false, + wordWrap: 'off', + lineNumbers: 'off', + lineDecorationsWidth: 0, + glyphMargin: false, + fixedOverflowWidgets: true, + minimap: { enabled: false }, + renderValidationDecorations: 'on', + renderLineHighlight: 'none', + readOnly: true +}; + +export function getOptimizedNestedCodeEditorWidgetOptions(): ICodeEditorWidgetOptions { + return { + isSimpleWidget: false, + contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + MenuPreventer.ID, + SelectionClipboardContributionID, + ContextMenuController.ID, + SuggestController.ID, + SnippetController2.ID, + TabCompletionController.ID, + AccessibilityHelpController.ID + ]) + }; +} + +export const fixedDiffEditorOptions: IDiffEditorOptions = { + ...fixedEditorOptions, + glyphMargin: true, + enableSplitViewResizing: false, + renderIndicators: false, + readOnly: false, + isInEmbeddedEditor: true, + renderOverviewRuler: false +}; + +class PropertyHeader extends Disposable { + protected _foldingIndicator!: HTMLElement; + protected _statusSpan!: HTMLElement; + protected _toolbar!: ToolBar; + protected _menu!: IMenu; + protected _propertyExpanded?: IContextKey; + + constructor( + readonly cell: DiffElementViewModelBase, + readonly propertyHeaderContainer: HTMLElement, + readonly notebookEditor: INotebookTextDiffEditor, + readonly accessor: { + updateInfoRendering: (renderOutput: boolean) => void; + checkIfModified: (cell: DiffElementViewModelBase) => boolean; + getFoldingState: (cell: DiffElementViewModelBase) => PropertyFoldingState; + updateFoldingState: (cell: DiffElementViewModelBase, newState: PropertyFoldingState) => void; + unChangedLabel: string; + changedLabel: string; + prefix: string; + menuId: MenuId; + }, + @IContextMenuService readonly contextMenuService: IContextMenuService, + @IKeybindingService readonly keybindingService: IKeybindingService, + @INotificationService readonly notificationService: INotificationService, + @IMenuService readonly menuService: IMenuService, + @IContextKeyService readonly contextKeyService: IContextKeyService + ) { + super(); + } + + buildHeader(): void { + let metadataChanged = this.accessor.checkIfModified(this.cell); + this._foldingIndicator = DOM.append(this.propertyHeaderContainer, DOM.$('.property-folding-indicator')); + this._foldingIndicator.classList.add(this.accessor.prefix); + this._updateFoldingIcon(); + const metadataStatus = DOM.append(this.propertyHeaderContainer, DOM.$('div.property-status')); + this._statusSpan = DOM.append(metadataStatus, DOM.$('span')); + + if (metadataChanged) { + this._statusSpan.textContent = this.accessor.changedLabel; + this._statusSpan.style.fontWeight = 'bold'; + this.propertyHeaderContainer.classList.add('modified'); + } else { + this._statusSpan.textContent = this.accessor.unChangedLabel; + } + + const cellToolbarContainer = DOM.append(this.propertyHeaderContainer, DOM.$('div.property-toolbar')); + this._toolbar = new ToolBar(cellToolbarContainer, this.contextMenuService, { + actionViewItemProvider: action => { + if (action instanceof MenuItemAction) { + const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService); + return item; + } + + return undefined; + } + }); + this._register(this._toolbar); + this._toolbar.context = { + cell: this.cell + }; + + const scopedContextKeyService = this.contextKeyService.createScoped(cellToolbarContainer); + this._register(scopedContextKeyService); + const propertyChanged = NOTEBOOK_DIFF_CELL_PROPERTY.bindTo(scopedContextKeyService); + propertyChanged.set(metadataChanged); + this._propertyExpanded = NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED.bindTo(scopedContextKeyService); + + this._menu = this.menuService.createMenu(this.accessor.menuId, scopedContextKeyService); + this._register(this._menu); + + const actions: IAction[] = []; + createAndFillInActionBarActions(this._menu, { shouldForwardArgs: true }, actions); + this._toolbar.setActions(actions); + + this._register(this._menu.onDidChange(() => { + const actions: IAction[] = []; + createAndFillInActionBarActions(this._menu, { shouldForwardArgs: true }, actions); + this._toolbar.setActions(actions); + })); + + this._register(this.notebookEditor.onMouseUp(e => { + if (!e.event.target) { + return; + } + + const target = e.event.target as HTMLElement; + + if (target.classList.contains('codicon-notebook-collapsed') || target.classList.contains('codicon-notebook-expanded')) { + const parent = target.parentElement as HTMLElement; + + if (!parent) { + return; + } + + if (!parent.classList.contains(this.accessor.prefix)) { + return; + } + + if (!parent.classList.contains('property-folding-indicator')) { + return; + } + + // folding icon + + const cellViewModel = e.target; + + if (cellViewModel === this.cell) { + const oldFoldingState = this.accessor.getFoldingState(this.cell); + this.accessor.updateFoldingState(this.cell, oldFoldingState === PropertyFoldingState.Expanded ? PropertyFoldingState.Collapsed : PropertyFoldingState.Expanded); + this._updateFoldingIcon(); + this.accessor.updateInfoRendering(this.cell.renderOutput); + } + } + + return; + })); + + this._updateFoldingIcon(); + this.accessor.updateInfoRendering(this.cell.renderOutput); + } + + refresh() { + let metadataChanged = this.accessor.checkIfModified(this.cell); + if (metadataChanged) { + this._statusSpan.textContent = this.accessor.changedLabel; + this._statusSpan.style.fontWeight = 'bold'; + this.propertyHeaderContainer.classList.add('modified'); + const actions: IAction[] = []; + createAndFillInActionBarActions(this._menu, undefined, actions); + this._toolbar.setActions(actions); + } else { + this._statusSpan.textContent = this.accessor.unChangedLabel; + this._statusSpan.style.fontWeight = 'normal'; + this._toolbar.setActions([]); + } + } + + private _updateFoldingIcon() { + if (this.accessor.getFoldingState(this.cell) === PropertyFoldingState.Collapsed) { + DOM.reset(this._foldingIndicator, renderIcon(collapsedIcon)); + this._propertyExpanded?.set(false); + } else { + DOM.reset(this._foldingIndicator, renderIcon(expandedIcon)); + this._propertyExpanded?.set(true); + } + + } +} + +abstract class AbstractElementRenderer extends Disposable { + protected _metadataHeaderContainer!: HTMLElement; + protected _metadataHeader!: PropertyHeader; + protected _metadataInfoContainer!: HTMLElement; + protected _metadataEditorContainer?: HTMLElement; + protected _metadataEditorDisposeStore!: DisposableStore; + protected _metadataEditor?: CodeEditorWidget | DiffEditorWidget; + + protected _outputHeaderContainer!: HTMLElement; + protected _outputHeader!: PropertyHeader; + protected _outputInfoContainer!: HTMLElement; + protected _outputEditorContainer?: HTMLElement; + protected _outputViewContainer?: HTMLElement; + protected _outputLeftContainer?: HTMLElement; + protected _outputRightContainer?: HTMLElement; + protected _outputEmptyElement?: HTMLElement; + protected _outputLeftView?: OutputContainer; + protected _outputRightView?: OutputContainer; + protected _outputEditorDisposeStore!: DisposableStore; + protected _outputEditor?: CodeEditorWidget | DiffEditorWidget; + + + protected _diffEditorContainer!: HTMLElement; + protected _diagonalFill?: HTMLElement; + protected _isDisposed: boolean; + + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: DiffElementViewModelBase, + readonly templateData: CellDiffSingleSideRenderTemplate | CellDiffSideBySideRenderTemplate, + readonly style: 'left' | 'right' | 'full', + protected readonly instantiationService: IInstantiationService, + protected readonly modeService: IModeService, + protected readonly modelService: IModelService, + protected readonly textModelService: ITextModelService, + protected readonly contextMenuService: IContextMenuService, + protected readonly keybindingService: IKeybindingService, + protected readonly notificationService: INotificationService, + protected readonly menuService: IMenuService, + protected readonly contextKeyService: IContextKeyService + + + ) { + super(); + // init + this._isDisposed = false; + this._metadataEditorDisposeStore = new DisposableStore(); + this._outputEditorDisposeStore = new DisposableStore(); + this._register(this._metadataEditorDisposeStore); + this._register(this._outputEditorDisposeStore); + this._register(cell.onDidLayoutChange(e => this.layout(e))); + this._register(cell.onDidLayoutChange(e => this.updateBorders())); + this.buildBody(); + + this._register(cell.onDidStateChange(() => { + this.updateOutputRendering(this.cell.renderOutput); + })); + } + + abstract buildBody(): void; + + updateMetadataRendering() { + if (this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { + // we should expand the metadata editor + this._metadataInfoContainer.style.display = 'block'; + + if (!this._metadataEditorContainer || !this._metadataEditor) { + // create editor + this._metadataEditorContainer = DOM.append(this._metadataInfoContainer, DOM.$('.metadata-editor-container')); + this._buildMetadataEditor(); + } else { + this.cell.metadataHeight = this._metadataEditor.getContentHeight(); + } + } else { + // we should collapse the metadata editor + this._metadataInfoContainer.style.display = 'none'; + // this._metadataEditorDisposeStore.clear(); + this.cell.metadataHeight = 0; + } + } + + updateOutputRendering(renderRichOutput: boolean) { + if (this.cell.outputFoldingState === PropertyFoldingState.Expanded) { + this._outputInfoContainer.style.display = 'block'; + if (renderRichOutput) { + this._hideOutputsRaw(); + this._buildOutputRendererContainer(); + this._showOutputsRenderer(); + this._showOutputsEmptyView(); + } else { + this._hideOutputsRenderer(); + this._buildOutputRawContainer(); + this._showOutputsRaw(); + } + } else { + this._outputInfoContainer.style.display = 'none'; + + this._hideOutputsRaw(); + this._hideOutputsRenderer(); + this._hideOutputsEmptyView(); + } + } + + private _buildOutputRawContainer() { + if (!this._outputEditorContainer) { + this._outputEditorContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-editor-container')); + this._buildOutputEditor(); + } + } + + private _showOutputsRaw() { + if (this._outputEditorContainer) { + this._outputEditorContainer.style.display = 'block'; + this.cell.rawOutputHeight = this._outputEditor!.getContentHeight(); + } + } + + private _showOutputsEmptyView() { + this.cell.layoutChange(); + } + + private _hideOutputsRaw() { + if (this._outputEditorContainer) { + this._outputEditorContainer.style.display = 'none'; + this.cell.rawOutputHeight = 0; + } + } + + private _hideOutputsEmptyView() { + this.cell.layoutChange(); + } + + abstract _buildOutputRendererContainer(): void; + abstract _hideOutputsRenderer(): void; + abstract _showOutputsRenderer(): void; + + private _applySanitizedMetadataChanges(currentMetadata: NotebookCellMetadata, newMetadata: any) { + let result: { [key: string]: any } = {}; + let newLangauge: string | undefined = undefined; + try { + const newMetadataObj = JSON.parse(newMetadata); + const keys = new Set([...Object.keys(newMetadataObj)]); + for (let key of keys) { + switch (key as keyof NotebookCellMetadata) { + case 'breakpointMargin': + case 'editable': + case 'hasExecutionOrder': + case 'inputCollapsed': + case 'outputCollapsed': + case 'runnable': + // boolean + if (typeof newMetadataObj[key] === 'boolean') { + result[key] = newMetadataObj[key]; + } else { + result[key] = currentMetadata[key as keyof NotebookCellMetadata]; + } + break; + + case 'executionOrder': + case 'lastRunDuration': + // number + if (typeof newMetadataObj[key] === 'number') { + result[key] = newMetadataObj[key]; + } else { + result[key] = currentMetadata[key as keyof NotebookCellMetadata]; + } + break; + case 'runState': + // enum + if (typeof newMetadataObj[key] === 'number' && [1, 2, 3, 4].indexOf(newMetadataObj[key]) >= 0) { + result[key] = newMetadataObj[key]; + } else { + result[key] = currentMetadata[key as keyof NotebookCellMetadata]; + } + break; + case 'statusMessage': + // string + if (typeof newMetadataObj[key] === 'string') { + result[key] = newMetadataObj[key]; + } else { + result[key] = currentMetadata[key as keyof NotebookCellMetadata]; + } + break; + default: + if (key === 'language') { + newLangauge = newMetadataObj[key]; + } + result[key] = newMetadataObj[key]; + break; + } + } + + if (newLangauge !== undefined && newLangauge !== this.cell.modified!.language) { + const index = this.notebookEditor.textModel!.cells.indexOf(this.cell.modified!.textModel); + this.notebookEditor.textModel!.applyEdits( + this.notebookEditor.textModel!.versionId, + [{ editType: CellEditType.CellLanguage, index, language: newLangauge }], + true, + undefined, + () => undefined, + undefined + ); + } + + const index = this.notebookEditor.textModel!.cells.indexOf(this.cell.modified!.textModel); + + if (index < 0) { + return; + } + + this.notebookEditor.textModel!.applyEdits(this.notebookEditor.textModel!.versionId, [ + { editType: CellEditType.Metadata, index, metadata: result } + ], true, undefined, () => undefined, undefined); + } catch { + } + } + + private async _buildMetadataEditor() { + this._metadataEditorDisposeStore.clear(); + + if (this.cell instanceof SideBySideDiffElementViewModel) { + this._metadataEditor = this.instantiationService.createInstance(DiffEditorWidget, this._metadataEditorContainer!, { + ...fixedDiffEditorOptions, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + readOnly: false, + originalEditable: false, + ignoreTrimWhitespace: false, + automaticLayout: false, + dimension: { + height: this.cell.layoutInfo.metadataHeight, + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), true, true) + } + }, { + originalEditor: getOptimizedNestedCodeEditorWidgetOptions(), + modifiedEditor: getOptimizedNestedCodeEditorWidgetOptions() + }); + this.layout({ metadataHeight: true }); + this._metadataEditorDisposeStore.add(this._metadataEditor); + + this._metadataEditorContainer?.classList.add('diff'); + + const originalMetadataModel = await this.textModelService.createModelReference(CellUri.generateCellMetadataUri(this.cell.originalDocument.uri, this.cell.original!.handle)); + const modifiedMetadataModel = await this.textModelService.createModelReference(CellUri.generateCellMetadataUri(this.cell.modifiedDocument.uri, this.cell.modified!.handle)); + this._metadataEditor.setModel({ + original: originalMetadataModel.object.textEditorModel, + modified: modifiedMetadataModel.object.textEditorModel + }); + + this._metadataEditorDisposeStore.add(originalMetadataModel); + this._metadataEditorDisposeStore.add(modifiedMetadataModel); + + this.cell.metadataHeight = this._metadataEditor.getContentHeight(); + + this._metadataEditorDisposeStore.add(this._metadataEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { + this.cell.metadataHeight = e.contentHeight; + } + })); + + let respondingToContentChange = false; + + this._metadataEditorDisposeStore.add(modifiedMetadataModel.object.textEditorModel.onDidChangeContent(() => { + respondingToContentChange = true; + const value = modifiedMetadataModel.object.textEditorModel.getValue(); + this._applySanitizedMetadataChanges(this.cell.modified!.metadata, value); + this._metadataHeader.refresh(); + respondingToContentChange = false; + })); + + this._metadataEditorDisposeStore.add(this.cell.modified!.textModel.onDidChangeMetadata(() => { + if (respondingToContentChange) { + return; + } + + const modifiedMetadataSource = getFormatedMetadataJSON(this.notebookEditor.textModel!, this.cell.modified?.metadata || {}, this.cell.modified?.language); + modifiedMetadataModel.object.textEditorModel.setValue(modifiedMetadataSource); + })); + + return; + } else { + this._metadataEditor = this.instantiationService.createInstance(CodeEditorWidget, this._metadataEditorContainer!, { + ...fixedEditorOptions, + dimension: { + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), + height: this.cell.layoutInfo.metadataHeight + }, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + readOnly: false + }, {}); + this.layout({ metadataHeight: true }); + this._metadataEditorDisposeStore.add(this._metadataEditor); + + const mode = this.modeService.create('jsonc'); + const originalMetadataSource = getFormatedMetadataJSON(this.notebookEditor.textModel!, + this.cell.type === 'insert' + ? this.cell.modified!.metadata || {} + : this.cell.original!.metadata || {}); + const uri = this.cell.type === 'insert' + ? this.cell.modified!.uri + : this.cell.original!.uri; + const handle = this.cell.type === 'insert' + ? this.cell.modified!.handle + : this.cell.original!.handle; + + const modelUri = CellUri.generateCellMetadataUri(uri, handle); + const metadataModel = this.modelService.createModel(originalMetadataSource, mode, modelUri, false); + this._metadataEditor.setModel(metadataModel); + this._metadataEditorDisposeStore.add(metadataModel); + + this.cell.metadataHeight = this._metadataEditor.getContentHeight(); + + this._metadataEditorDisposeStore.add(this._metadataEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { + this.cell.metadataHeight = e.contentHeight; + } + })); + } + } + + private _getFormatedOutputJSON(outputs: IProcessedOutput[]) { + return JSON.stringify(outputs, undefined, '\t'); + } + + private _buildOutputEditor() { + this._outputEditorDisposeStore.clear(); + + if ((this.cell.type === 'modified' || this.cell.type === 'unchanged') && !this.notebookEditor.textModel!.transientOptions.transientOutputs) { + const originalOutputsSource = this._getFormatedOutputJSON(this.cell.original?.outputs || []); + const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []); + if (originalOutputsSource !== modifiedOutputsSource) { + const mode = this.modeService.create('json'); + const originalModel = this.modelService.createModel(originalOutputsSource, mode, undefined, true); + const modifiedModel = this.modelService.createModel(modifiedOutputsSource, mode, undefined, true); + this._outputEditorDisposeStore.add(originalModel); + this._outputEditorDisposeStore.add(modifiedModel); + + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const lineCount = Math.max(originalModel.getLineCount(), modifiedModel.getLineCount()); + this._outputEditor = this.instantiationService.createInstance(DiffEditorWidget, this._outputEditorContainer!, { + ...fixedDiffEditorOptions, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + readOnly: true, + ignoreTrimWhitespace: false, + automaticLayout: false, + dimension: { + height: Math.min(OUTPUT_EDITOR_HEIGHT_MAGIC, this.cell.layoutInfo.rawOutputHeight || lineHeight * lineCount), + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true) + } + }, { + originalEditor: getOptimizedNestedCodeEditorWidgetOptions(), + modifiedEditor: getOptimizedNestedCodeEditorWidgetOptions() + }); + this._outputEditorDisposeStore.add(this._outputEditor); + + this._outputEditorContainer?.classList.add('diff'); + + this._outputEditor.setModel({ + original: originalModel, + modified: modifiedModel + }); + this._outputEditor.restoreViewState(this.cell.getOutputEditorViewState() as editorCommon.IDiffEditorViewState); + + this.cell.rawOutputHeight = this._outputEditor.getContentHeight(); + + this._outputEditorDisposeStore.add(this._outputEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) { + this.cell.rawOutputHeight = e.contentHeight; + } + })); + + this._outputEditorDisposeStore.add(this.cell.modified!.textModel.onDidChangeOutputs(() => { + const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []); + modifiedModel.setValue(modifiedOutputsSource); + this._outputHeader.refresh(); + })); + + return; + } + } + + this._outputEditor = this.instantiationService.createInstance(CodeEditorWidget, this._outputEditorContainer!, { + ...fixedEditorOptions, + dimension: { + width: Math.min(OUTPUT_EDITOR_HEIGHT_MAGIC, this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, this.cell.type === 'unchanged' || this.cell.type === 'modified') - 32), + height: this.cell.layoutInfo.rawOutputHeight + }, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() + }, {}); + this._outputEditorDisposeStore.add(this._outputEditor); + + const mode = this.modeService.create('json'); + const originaloutputSource = this._getFormatedOutputJSON( + this.notebookEditor.textModel!.transientOptions.transientOutputs + ? [] + : this.cell.type === 'insert' + ? this.cell.modified!.outputs || [] + : this.cell.original!.outputs || []); + const outputModel = this.modelService.createModel(originaloutputSource, mode, undefined, true); + this._outputEditorDisposeStore.add(outputModel); + this._outputEditor.setModel(outputModel); + this._outputEditor.restoreViewState(this.cell.getOutputEditorViewState()); + + this.cell.rawOutputHeight = this._outputEditor.getContentHeight(); + + this._outputEditorDisposeStore.add(this._outputEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) { + this.cell.rawOutputHeight = e.contentHeight; + } + })); + } + + protected layoutNotebookCell() { + this.notebookEditor.layoutNotebookCell( + this.cell, + this.cell.layoutInfo.totalHeight + ); + } + + updateBorders() { + this.templateData.leftBorder.style.height = `${this.cell.layoutInfo.totalHeight - 32}px`; + this.templateData.rightBorder.style.height = `${this.cell.layoutInfo.totalHeight - 32}px`; + this.templateData.bottomBorder.style.top = `${this.cell.layoutInfo.totalHeight - 32}px`; + } + + dispose() { + if (this._outputEditor) { + this.cell.saveOutputEditorViewState(this._outputEditor.saveViewState()); + } + + if (this._metadataEditor) { + this.cell.saveMetadataEditorViewState(this._metadataEditor.saveViewState()); + } + + this._metadataEditorDisposeStore.dispose(); + this._outputEditorDisposeStore.dispose(); + + this._isDisposed = true; + super.dispose(); + } + + abstract styleContainer(container: HTMLElement): void; + abstract updateSourceEditor(): void; + abstract layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, metadataHeight?: boolean, outputEditor?: boolean, outputView?: boolean }): void; +} + +abstract class SingleSideDiffElement extends AbstractElementRenderer { + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: SingleSideDiffElementViewModel, + readonly templateData: CellDiffSingleSideRenderTemplate, + readonly style: 'left' | 'right' | 'full', + protected readonly instantiationService: IInstantiationService, + protected readonly modeService: IModeService, + protected readonly modelService: IModelService, + protected readonly textModelService: ITextModelService, + protected readonly contextMenuService: IContextMenuService, + protected readonly keybindingService: IKeybindingService, + protected readonly notificationService: INotificationService, + protected readonly menuService: IMenuService, + protected readonly contextKeyService: IContextKeyService + + + ) { + super( + notebookEditor, + cell, + templateData, + style, + instantiationService, + modeService, + modelService, + textModelService, + contextMenuService, + keybindingService, + notificationService, + menuService, + contextKeyService + ); + } + + buildBody() { + const body = this.templateData.body; + this._diffEditorContainer = this.templateData.diffEditorContainer; + switch (this.style) { + case 'left': + body.classList.add('left'); + break; + case 'right': + body.classList.add('right'); + break; + default: + body.classList.add('full'); + break; + } + + this._diagonalFill = this.templateData.diagonalFill; + this.styleContainer(this._diffEditorContainer); + this.updateSourceEditor(); + + this._metadataHeaderContainer = this.templateData.metadataHeaderContainer; + this._metadataInfoContainer = this.templateData.metadataInfoContainer; + this._metadataHeaderContainer.innerText = ''; + this._metadataInfoContainer.innerText = ''; + + this._metadataHeader = this.instantiationService.createInstance( + PropertyHeader, + this.cell, + this._metadataHeaderContainer, + this.notebookEditor, + { + updateInfoRendering: this.updateMetadataRendering.bind(this), + checkIfModified: (cell) => { + return cell.checkMetadataIfModified(); + }, + getFoldingState: (cell) => { + return cell.metadataFoldingState; + }, + updateFoldingState: (cell, state) => { + cell.metadataFoldingState = state; + }, + unChangedLabel: 'Metadata', + changedLabel: 'Metadata changed', + prefix: 'metadata', + menuId: MenuId.NotebookDiffCellMetadataTitle + } + ); + this._register(this._metadataHeader); + this._metadataHeader.buildHeader(); + + if (this.notebookEditor.textModel?.transientOptions.transientOutputs) { + this.cell.rawOutputHeight = 0; + this.cell.outputStatusHeight = 0; + this.templateData.outputHeaderContainer.style.display = 'none'; + this.templateData.outputInfoContainer.style.display = 'none'; + return; + } else { + this.templateData.outputHeaderContainer.style.display = 'flex'; + this.templateData.outputInfoContainer.style.display = 'block'; + + } + + this._outputHeaderContainer = this.templateData.outputHeaderContainer; + this._outputInfoContainer = this.templateData.outputInfoContainer; + + this._outputHeaderContainer.innerText = ''; + this._outputInfoContainer.innerText = ''; + + this._outputHeader = this.instantiationService.createInstance( + PropertyHeader, + this.cell, + this._outputHeaderContainer, + this.notebookEditor, + { + updateInfoRendering: this.updateOutputRendering.bind(this), + checkIfModified: (cell) => { + return cell.checkIfOutputsModified(); + }, + getFoldingState: (cell) => { + return cell.outputFoldingState; + }, + updateFoldingState: (cell, state) => { + cell.outputFoldingState = state; + }, + unChangedLabel: 'Outputs', + changedLabel: 'Outputs changed', + prefix: 'output', + menuId: MenuId.NotebookDiffCellOutputsTitle + } + ); + this._register(this._outputHeader); + this._outputHeader.buildHeader(); + } +} +export class DeletedElement extends SingleSideDiffElement { + private _editor!: CodeEditorWidget; + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: SingleSideDiffElementViewModel, + readonly templateData: CellDiffSingleSideRenderTemplate, + @IModeService readonly modeService: IModeService, + @IModelService readonly modelService: IModelService, + @ITextModelService readonly textModelService: ITextModelService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IContextMenuService protected readonly contextMenuService: IContextMenuService, + @IKeybindingService protected readonly keybindingService: IKeybindingService, + @INotificationService protected readonly notificationService: INotificationService, + @IMenuService protected readonly menuService: IMenuService, + @IContextKeyService protected readonly contextKeyService: IContextKeyService, + + + ) { + super(notebookEditor, cell, templateData, 'left', instantiationService, modeService, modelService, textModelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService); + } + + styleContainer(container: HTMLElement) { + container.classList.remove('inserted'); + container.classList.add('removed'); + } + + updateSourceEditor(): void { + const originalCell = this.cell.original!; + const lineCount = originalCell.textModel.textBuffer.getLineCount(); + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const editorHeight = lineCount * lineHeight + getEditorTopPadding() + EDITOR_BOTTOM_PADDING; + + this._editor = this.templateData.sourceEditor; + this._editor.layout({ + width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18, + height: editorHeight + }); + + this.cell.editorHeight = editorHeight; + + this._register(this._editor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.layoutInfo.editorHeight !== e.contentHeight) { + this.cell.editorHeight = e.contentHeight; + } + })); + + originalCell.textModel.resolveTextModelRef().then(ref => { + if (this._isDisposed) { + return; + } + + this._register(ref); + + const textModel = ref.object.textEditorModel; + this._editor.setModel(textModel); + this.cell.editorHeight = this._editor.getContentHeight(); + }); + } + + layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataHeight?: boolean, outputTotalHeight?: boolean }) { + DOM.scheduleAtNextAnimationFrame(() => { + if (state.editorHeight || state.outerWidth) { + this._editor.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this.cell.layoutInfo.editorHeight + }); + } + + if (state.metadataHeight || state.outerWidth) { + this._metadataEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this.cell.layoutInfo.metadataHeight + }); + } + + if (state.outputTotalHeight || state.outerWidth) { + this._outputEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this.cell.layoutInfo.outputTotalHeight + }); + } + + if (this._diagonalFill) { + this._diagonalFill.style.height = `${this.cell.layoutInfo.totalHeight - 32}px`; + } + + this.layoutNotebookCell(); + }); + } + + _buildOutputRendererContainer() { + if (!this._outputViewContainer) { + this._outputViewContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-view-container')); + this._outputEmptyElement = DOM.append(this._outputViewContainer, DOM.$('.output-empty-view')); + const span = DOM.append(this._outputEmptyElement, DOM.$('span')); + span.innerText = 'No outputs to render'; + + if (this.cell.original!.outputs.length === 0) { + this._outputEmptyElement.style.display = 'block'; + } else { + this._outputEmptyElement.style.display = 'none'; + } + + this.cell.layoutChange(); + + this._outputLeftView = this.instantiationService.createInstance(OutputContainer, this.notebookEditor, this.notebookEditor.textModel!, this.cell, this.cell.original!, DiffSide.Original, this._outputViewContainer!); + this._register(this._outputLeftView); + this._outputLeftView.render(); + + const removedOutputRenderListener = this.notebookEditor.onDidDynamicOutputRendered(e => { + if (e.cell.uri.toString() === this.cell.original!.uri.toString()) { + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Original, this.cell.original!.id, ['nb-cellDeleted'], []); + removedOutputRenderListener.dispose(); + } + }); + + this._register(removedOutputRenderListener); + } + + this._outputViewContainer.style.display = 'block'; + } + + _decorate() { + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Original, this.cell.original!.id, ['nb-cellDeleted'], []); + } + + _showOutputsRenderer() { + if (this._outputViewContainer) { + this._outputViewContainer.style.display = 'block'; + + this._outputLeftView?.showOutputs(); + this._decorate(); + } + } + + _hideOutputsRenderer() { + if (this._outputViewContainer) { + this._outputViewContainer.style.display = 'none'; + + this._outputLeftView?.hideOutputs(); + } + } + + dispose() { + if (this._editor) { + this.cell.saveSpirceEditorViewState(this._editor.saveViewState()); + } + + super.dispose(); + } +} + +export class InsertElement extends SingleSideDiffElement { + private _editor!: CodeEditorWidget; + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: SingleSideDiffElementViewModel, + readonly templateData: CellDiffSingleSideRenderTemplate, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IModeService readonly modeService: IModeService, + @IModelService readonly modelService: IModelService, + @ITextModelService readonly textModelService: ITextModelService, + @IContextMenuService protected readonly contextMenuService: IContextMenuService, + @IKeybindingService protected readonly keybindingService: IKeybindingService, + @INotificationService protected readonly notificationService: INotificationService, + @IMenuService protected readonly menuService: IMenuService, + @IContextKeyService protected readonly contextKeyService: IContextKeyService, + ) { + super(notebookEditor, cell, templateData, 'right', instantiationService, modeService, modelService, textModelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService); + } + + styleContainer(container: HTMLElement): void { + container.classList.remove('removed'); + container.classList.add('inserted'); + } + + updateSourceEditor(): void { + const modifiedCell = this.cell.modified!; + const lineCount = modifiedCell.textModel.textBuffer.getLineCount(); + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const editorHeight = lineCount * lineHeight + getEditorTopPadding() + EDITOR_BOTTOM_PADDING; + + this._editor = this.templateData.sourceEditor; + this._editor.layout( + { + width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18, + height: editorHeight + } + ); + this._editor.updateOptions({ readOnly: false }); + this.cell.editorHeight = editorHeight; + + this._register(this._editor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.layoutInfo.editorHeight !== e.contentHeight) { + this.cell.editorHeight = e.contentHeight; + } + })); + + modifiedCell.textModel.resolveTextModelRef().then(ref => { + if (this._isDisposed) { + return; + } + + this._register(ref); + + const textModel = ref.object.textEditorModel; + this._editor.setModel(textModel); + this._editor.restoreViewState(this.cell.getSourceEditorViewState() as editorCommon.ICodeEditorViewState); + this.cell.editorHeight = this._editor.getContentHeight(); + }); + } + + _buildOutputRendererContainer() { + if (!this._outputViewContainer) { + this._outputViewContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-view-container')); + this._outputEmptyElement = DOM.append(this._outputViewContainer, DOM.$('.output-empty-view')); + this._outputEmptyElement.innerText = 'No outputs to render'; + + if (this.cell.modified!.outputs.length === 0) { + this._outputEmptyElement.style.display = 'block'; + } else { + this._outputEmptyElement.style.display = 'none'; + } + + this.cell.layoutChange(); + + this._outputRightView = this.instantiationService.createInstance(OutputContainer, this.notebookEditor, this.notebookEditor.textModel!, this.cell, this.cell.modified!, DiffSide.Modified, this._outputViewContainer!); + this._register(this._outputRightView); + this._outputRightView.render(); + + const insertOutputRenderListener = this.notebookEditor.onDidDynamicOutputRendered(e => { + if (e.cell.uri.toString() === this.cell.modified!.uri.toString()) { + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Modified, this.cell.modified!.id, ['nb-cellAdded'], []); + insertOutputRenderListener.dispose(); + } + }); + this._register(insertOutputRenderListener); + } + + this._outputViewContainer.style.display = 'block'; + } + + _decorate() { + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Modified, this.cell.modified!.id, ['nb-cellAdded'], []); + } + + _showOutputsRenderer() { + if (this._outputViewContainer) { + this._outputViewContainer.style.display = 'block'; + this._outputRightView?.showOutputs(); + this._decorate(); + } + } + + _hideOutputsRenderer() { + if (this._outputViewContainer) { + this._outputViewContainer.style.display = 'none'; + this._outputRightView?.hideOutputs(); + } + } + + layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataHeight?: boolean, outputTotalHeight?: boolean }) { + DOM.scheduleAtNextAnimationFrame(() => { + if (state.editorHeight || state.outerWidth) { + this._editor.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this.cell.layoutInfo.editorHeight + }); + } + + if (state.metadataHeight || state.outerWidth) { + this._metadataEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), + height: this.cell.layoutInfo.metadataHeight + }); + } + + if (state.outputTotalHeight || state.outerWidth) { + this._outputEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this.cell.layoutInfo.outputTotalHeight + }); + } + + this.layoutNotebookCell(); + + if (this._diagonalFill) { + this._diagonalFill.style.height = `${this.cell.layoutInfo.editorHeight + this.cell.layoutInfo.editorMargin + this.cell.layoutInfo.metadataStatusHeight + this.cell.layoutInfo.metadataHeight + this.cell.layoutInfo.outputTotalHeight + this.cell.layoutInfo.outputStatusHeight}px`; + } + }); + } + + dispose() { + if (this._editor) { + this.cell.saveSpirceEditorViewState(this._editor.saveViewState()); + } + + super.dispose(); + } +} + +export class ModifiedElement extends AbstractElementRenderer { + private _editor?: DiffEditorWidget; + private _editorContainer!: HTMLElement; + private _inputToolbarContainer!: HTMLElement; + protected _toolbar!: ToolBar; + protected _menu!: IMenu; + + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: SideBySideDiffElementViewModel, + readonly templateData: CellDiffSideBySideRenderTemplate, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IModeService readonly modeService: IModeService, + @IModelService readonly modelService: IModelService, + @ITextModelService readonly textModelService: ITextModelService, + @IContextMenuService protected readonly contextMenuService: IContextMenuService, + @IKeybindingService protected readonly keybindingService: IKeybindingService, + @INotificationService protected readonly notificationService: INotificationService, + @IMenuService protected readonly menuService: IMenuService, + @IContextKeyService protected readonly contextKeyService: IContextKeyService + ) { + super(notebookEditor, cell, templateData, 'full', instantiationService, modeService, modelService, textModelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService); + } + + styleContainer(container: HTMLElement): void { + container.classList.remove('inserted', 'removed'); + } + + buildBody() { + const body = this.templateData.body; + this._diffEditorContainer = this.templateData.diffEditorContainer; + body.classList.remove('left', 'right', 'full'); + switch (this.style) { + case 'left': + body.classList.add('left'); + break; + case 'right': + body.classList.add('right'); + break; + default: + body.classList.add('full'); + break; + } + + this.styleContainer(this._diffEditorContainer); + this.updateSourceEditor(); + + this._metadataHeaderContainer = this.templateData.metadataHeaderContainer; + this._metadataInfoContainer = this.templateData.metadataInfoContainer; + + this._metadataHeaderContainer.innerText = ''; + this._metadataInfoContainer.innerText = ''; + + this._metadataHeader = this.instantiationService.createInstance( + PropertyHeader, + this.cell, + this._metadataHeaderContainer, + this.notebookEditor, + { + updateInfoRendering: this.updateMetadataRendering.bind(this), + checkIfModified: (cell) => { + return cell.checkMetadataIfModified(); + }, + getFoldingState: (cell) => { + return cell.metadataFoldingState; + }, + updateFoldingState: (cell, state) => { + cell.metadataFoldingState = state; + }, + unChangedLabel: 'Metadata', + changedLabel: 'Metadata changed', + prefix: 'metadata', + menuId: MenuId.NotebookDiffCellMetadataTitle + } + ); + this._register(this._metadataHeader); + this._metadataHeader.buildHeader(); + + if (this.notebookEditor.textModel?.transientOptions.transientOutputs) { + this.cell.rawOutputHeight = 0; + this.cell.outputStatusHeight = 0; + this.templateData.outputHeaderContainer.style.display = 'none'; + this.templateData.outputInfoContainer.style.display = 'none'; + return; + } else { + this.templateData.outputHeaderContainer.style.display = 'flex'; + this.templateData.outputInfoContainer.style.display = 'block'; + } + + this._outputHeaderContainer = this.templateData.outputHeaderContainer; + this._outputInfoContainer = this.templateData.outputInfoContainer; + this._outputHeaderContainer.innerText = ''; + this._outputInfoContainer.innerText = ''; + + if (this.cell.checkIfOutputsModified()) { + this._outputInfoContainer.classList.add('modified'); + } + + this._outputHeader = this.instantiationService.createInstance( + PropertyHeader, + this.cell, + this._outputHeaderContainer, + this.notebookEditor, + { + updateInfoRendering: this.updateOutputRendering.bind(this), + checkIfModified: (cell) => { + return cell.checkIfOutputsModified(); + }, + getFoldingState: (cell) => { + return cell.outputFoldingState; + }, + updateFoldingState: (cell, state) => { + cell.outputFoldingState = state; + }, + unChangedLabel: 'Outputs', + changedLabel: 'Outputs changed', + prefix: 'output', + menuId: MenuId.NotebookDiffCellOutputsTitle + } + ); + this._register(this._outputHeader); + this._outputHeader.buildHeader(); + } + + _buildOutputRendererContainer() { + if (!this._outputViewContainer) { + this._outputViewContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-view-container')); + this._outputEmptyElement = DOM.append(this._outputViewContainer, DOM.$('.output-empty-view')); + this._outputEmptyElement.innerText = 'No outputs to render'; + + if (!this.cell.checkIfOutputsModified() && this.cell.modified.outputs.length === 0) { + this._outputEmptyElement.style.display = 'block'; + } else { + this._outputEmptyElement.style.display = 'none'; + } + + this.cell.layoutChange(); + + this._register(this.cell.modified.textModel.onDidChangeOutputs(() => { + // currently we only allow outputs change to the modified cell + if (!this.cell.checkIfOutputsModified() && this.cell.modified.outputs.length === 0) { + this._outputEmptyElement!.style.display = 'block'; + } else { + this._outputEmptyElement!.style.display = 'none'; + } + })); + + this._outputLeftContainer = DOM.append(this._outputViewContainer!, DOM.$('.output-view-container-left')); + this._outputRightContainer = DOM.append(this._outputViewContainer!, DOM.$('.output-view-container-right')); + // We should use the original text model here + this._outputLeftView = this.instantiationService.createInstance(OutputContainer, this.notebookEditor, this.notebookEditor.textModel!, this.cell, this.cell.original!, DiffSide.Original, this._outputLeftContainer!); + this._outputLeftView.render(); + this._register(this._outputLeftView); + this._outputRightView = this.instantiationService.createInstance(OutputContainer, this.notebookEditor, this.notebookEditor.textModel!, this.cell, this.cell.modified!, DiffSide.Modified, this._outputRightContainer!); + this._outputRightView.render(); + this._register(this._outputRightView); + + const originalOutputRenderListener = this.notebookEditor.onDidDynamicOutputRendered(e => { + if (e.cell.uri.toString() === this.cell.original.uri.toString()) { + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Original, this.cell.original.id, ['nb-cellDeleted'], []); + originalOutputRenderListener.dispose(); + } + }); + + const modifiedOutputRenderListener = this.notebookEditor.onDidDynamicOutputRendered(e => { + if (e.cell.uri.toString() === this.cell.modified.uri.toString()) { + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Modified, this.cell.modified.id, ['nb-cellAdded'], []); + modifiedOutputRenderListener.dispose(); + } + }); + + this._register(originalOutputRenderListener); + this._register(modifiedOutputRenderListener); + + this._decorate(); + } + + this._outputViewContainer.style.display = 'block'; + } + + _decorate() { + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Original, this.cell.original.id, ['nb-cellDeleted'], []); + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Modified, this.cell.modified.id, ['nb-cellAdded'], []); + } + + _showOutputsRenderer() { + if (this._outputViewContainer) { + this._outputViewContainer.style.display = 'block'; + + this._outputLeftView?.showOutputs(); + this._outputRightView?.showOutputs(); + this._decorate(); + } + } + + _hideOutputsRenderer() { + if (this._outputViewContainer) { + this._outputViewContainer.style.display = 'none'; + + this._outputLeftView?.hideOutputs(); + this._outputRightView?.hideOutputs(); + } + } + + updateSourceEditor(): void { + const modifiedCell = this.cell.modified!; + const lineCount = modifiedCell.textModel.textBuffer.getLineCount(); + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const editorHeight = this.cell.layoutInfo.editorHeight !== 0 ? this.cell.layoutInfo.editorHeight : lineCount * lineHeight + getEditorTopPadding() + EDITOR_BOTTOM_PADDING; + this._editorContainer = this.templateData.editorContainer; + this._editor = this.templateData.sourceEditor; + + this._editorContainer.classList.add('diff'); + + this._editor.layout({ + width: this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN, + height: editorHeight + }); + + this._editorContainer.style.height = `${editorHeight}px`; + + this._register(this._editor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.layoutInfo.editorHeight !== e.contentHeight) { + this.cell.editorHeight = e.contentHeight; + } + })); + + this._initializeSourceDiffEditor(); + + this._inputToolbarContainer = this.templateData.inputToolbarContainer; + this._toolbar = this.templateData.toolbar; + + this._toolbar.context = { + cell: this.cell + }; + + this._menu = this.menuService.createMenu(MenuId.NotebookDiffCellInputTitle, this.contextKeyService); + this._register(this._menu); + const actions: IAction[] = []; + createAndFillInActionBarActions(this._menu, { shouldForwardArgs: true }, actions); + this._toolbar.setActions(actions); + + if (this.cell.modified!.textModel.getValue() !== this.cell.original!.textModel.getValue()) { + this._inputToolbarContainer.style.display = 'block'; + } else { + this._inputToolbarContainer.style.display = 'none'; + } + + this._register(this.cell.modified!.textModel.onDidChangeContent(() => { + if (this.cell.modified!.textModel.getValue() !== this.cell.original!.textModel.getValue()) { + this._inputToolbarContainer.style.display = 'block'; + } else { + this._inputToolbarContainer.style.display = 'none'; + } + })); + } + + private async _initializeSourceDiffEditor() { + const originalCell = this.cell.original!; + const modifiedCell = this.cell.modified!; + + const originalRef = await originalCell.textModel.resolveTextModelRef(); + const modifiedRef = await modifiedCell.textModel.resolveTextModelRef(); + + if (this._isDisposed) { + return; + } + + const textModel = originalRef.object.textEditorModel; + const modifiedTextModel = modifiedRef.object.textEditorModel; + this._register({ + dispose: () => { + const delayer = new Delayer(5000); + delayer.trigger(() => { + originalRef.dispose(); + delayer.dispose(); + }); + } + }); + this._register({ + dispose: () => { + const delayer = new Delayer(5000); + delayer.trigger(() => { + modifiedRef.dispose(); + delayer.dispose(); + }); + } + }); + + this._editor!.setModel({ + original: textModel, + modified: modifiedTextModel + }); + + this._editor!.restoreViewState(this.cell.getSourceEditorViewState() as editorCommon.IDiffEditorViewState); + + const contentHeight = this._editor!.getContentHeight(); + this.cell.editorHeight = contentHeight; + } + + layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataHeight?: boolean, outputTotalHeight?: boolean }) { + DOM.scheduleAtNextAnimationFrame(() => { + if (state.editorHeight) { + this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`; + this._editor!.layout({ + width: this._editor!.getViewWidth(), + height: this.cell.layoutInfo.editorHeight + }); + } + + if (state.outerWidth) { + this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`; + this._editor!.layout(); + } + + if (state.metadataHeight || state.outerWidth) { + if (this._metadataEditorContainer) { + this._metadataEditorContainer.style.height = `${this.cell.layoutInfo.metadataHeight}px`; + this._metadataEditor?.layout(); + } + } + + if (state.outputTotalHeight || state.outerWidth) { + if (this._outputEditorContainer) { + this._outputEditorContainer.style.height = `${this.cell.layoutInfo.outputTotalHeight}px`; + this._outputEditor?.layout(); + } + } + + + this.layoutNotebookCell(); + }); + } + + dispose() { + if (this._editor) { + this.cell.saveSpirceEditorViewState(this._editor.saveViewState()); + } + + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts new file mode 100644 index 00000000..74a0010f --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts @@ -0,0 +1,361 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import * as nls from 'vs/nls'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { DiffElementViewModelBase, SideBySideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { DiffSide, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; +import { ICellOutputViewModel, IDisplayOutputViewModel, IRenderOutput, outputHasDynamicHeight, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { BUILTIN_RENDERER_ID, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { DiffNestedCellViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { mimetypeIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; + +interface IMimeTypeRenderer extends IQuickPickItem { + index: number; +} + +export class OutputElement extends Disposable { + readonly resizeListener = new DisposableStore(); + domNode!: HTMLElement; + renderResult?: IRenderOutput; + + constructor( + private _notebookEditor: INotebookTextDiffEditor, + private _notebookTextModel: NotebookTextModel, + private _notebookService: INotebookService, + private _quickInputService: IQuickInputService, + private _diffElementViewModel: DiffElementViewModelBase, + private _diffSide: DiffSide, + private _nestedCell: DiffNestedCellViewModel, + private _outputContainer: HTMLElement, + readonly output: ICellOutputViewModel + ) { + super(); + } + + render(index: number, beforeElement?: HTMLElement) { + const outputItemDiv = document.createElement('div'); + let result: IRenderOutput | undefined = undefined; + + if (this.output.isDisplayOutput()) { + const [mimeTypes, pick] = this.output.resolveMimeTypes(this._notebookTextModel); + const pickedMimeTypeRenderer = mimeTypes[pick]; + if (mimeTypes.length > 1) { + outputItemDiv.style.position = 'relative'; + const mimeTypePicker = DOM.$('.multi-mimetype-output'); + mimeTypePicker.classList.add(...ThemeIcon.asClassNameArray(mimetypeIcon)); + mimeTypePicker.tabIndex = 0; + mimeTypePicker.title = nls.localize('mimeTypePicker', "Choose a different output mimetype, available mimetypes: {0}", mimeTypes.map(mimeType => mimeType.mimeType).join(', ')); + outputItemDiv.appendChild(mimeTypePicker); + this.resizeListener.add(DOM.addStandardDisposableListener(mimeTypePicker, 'mousedown', async e => { + if (e.leftButton) { + e.preventDefault(); + e.stopPropagation(); + await this.pickActiveMimeTypeRenderer(this._notebookTextModel, this.output as IDisplayOutputViewModel); + } + })); + + this.resizeListener.add((DOM.addDisposableListener(mimeTypePicker, DOM.EventType.KEY_DOWN, async e => { + const event = new StandardKeyboardEvent(e); + if ((event.equals(KeyCode.Enter) || event.equals(KeyCode.Space))) { + e.preventDefault(); + e.stopPropagation(); + await this.pickActiveMimeTypeRenderer(this._notebookTextModel, this.output as IDisplayOutputViewModel); + } + }))); + } + + const innerContainer = DOM.$('.output-inner-container'); + DOM.append(outputItemDiv, innerContainer); + + + if (pickedMimeTypeRenderer.rendererId !== BUILTIN_RENDERER_ID) { + const renderer = this._notebookService.getRendererInfo(pickedMimeTypeRenderer.rendererId); + result = renderer + ? { type: RenderOutputType.Extension, renderer, source: this.output, mimeType: pickedMimeTypeRenderer.mimeType } + : this._notebookEditor.getOutputRenderer().render(this.output, innerContainer, pickedMimeTypeRenderer.mimeType, this._notebookTextModel.uri,); + } else { + result = this._notebookEditor.getOutputRenderer().render(this.output, innerContainer, pickedMimeTypeRenderer.mimeType, this._notebookTextModel.uri); + } + + this.output.pickedMimeType = pick; + } else { + // for text and error, there is no mimetype + const innerContainer = DOM.$('.output-inner-container'); + DOM.append(outputItemDiv, innerContainer); + + result = this._notebookEditor.getOutputRenderer().render(this.output, innerContainer, undefined, this._notebookTextModel.uri); + } + + this.domNode = outputItemDiv; + this.renderResult = result; + + if (!result) { + // this.viewCell.updateOutputHeight(index, 0); + return; + } + + if (beforeElement) { + this._outputContainer.insertBefore(outputItemDiv, beforeElement); + } else { + this._outputContainer.appendChild(outputItemDiv); + } + + if (result.type !== RenderOutputType.None) { + // this.viewCell.selfSizeMonitoring = true; + this._notebookEditor.createInset( + this._diffElementViewModel, + this._nestedCell, + result, + () => this.getOutputOffsetInCell(index), + this._diffElementViewModel instanceof SideBySideDiffElementViewModel + ? this._diffSide + : this._diffElementViewModel.type === 'insert' ? DiffSide.Modified : DiffSide.Original + ); + } else { + outputItemDiv.classList.add('foreground', 'output-element'); + outputItemDiv.style.position = 'absolute'; + } + + if (outputHasDynamicHeight(result)) { + // this.viewCell.selfSizeMonitoring = true; + const clientHeight = outputItemDiv.clientHeight; + // TODO, set an inital dimension to avoid force reflow + // const dimension = { + // width: this.cellViewModel., + // height: clientHeight + // }; + + const elementSizeObserver = getResizesObserver(outputItemDiv, undefined, () => { + if (this._outputContainer && document.body.contains(this._outputContainer)) { + const height = Math.ceil(elementSizeObserver.getHeight()); + + if (clientHeight === height) { + return; + } + + const currIndex = this.getCellOutputCurrentIndex(); + if (currIndex < 0) { + return; + } + + this.updateHeight(currIndex, height); + } + }); + elementSizeObserver.startObserving(); + this.resizeListener.add(elementSizeObserver); + this.updateHeight(index, clientHeight); + } else if (result.type === RenderOutputType.None) { // no-op if it's a webview + const clientHeight = Math.ceil(outputItemDiv.clientHeight); + this.updateHeight(index, clientHeight); + + const top = this.getOutputOffsetInContainer(index); + outputItemDiv.style.top = `${top}px`; + } + } + + private async pickActiveMimeTypeRenderer(notebookTextModel: NotebookTextModel, viewModel: IDisplayOutputViewModel) { + const [mimeTypes, currIndex] = viewModel.resolveMimeTypes(notebookTextModel); + + const items = mimeTypes.filter(mimeType => mimeType.isTrusted).map((mimeType, index): IMimeTypeRenderer => ({ + label: mimeType.mimeType, + id: mimeType.mimeType, + index: index, + picked: index === currIndex, + detail: this.generateRendererInfo(mimeType.rendererId), + description: index === currIndex ? nls.localize('curruentActiveMimeType', "Currently Active") : undefined + })); + + const picker = this._quickInputService.createQuickPick(); + picker.items = items; + picker.activeItems = items.filter(item => !!item.picked); + picker.placeholder = items.length !== mimeTypes.length + ? nls.localize('promptChooseMimeTypeInSecure.placeHolder', "Select mimetype to render for current output. Rich mimetypes are available only when the notebook is trusted") + : nls.localize('promptChooseMimeType.placeHolder', "Select mimetype to render for current output"); + + const pick = await new Promise(resolve => { + picker.onDidAccept(() => { + resolve(picker.selectedItems.length === 1 ? (picker.selectedItems[0] as IMimeTypeRenderer).index : undefined); + picker.dispose(); + }); + picker.show(); + }); + + if (pick === undefined) { + return; + } + + if (pick !== currIndex) { + // user chooses another mimetype + const index = this._nestedCell.outputsViewModels.indexOf(viewModel); + const nextElement = this.domNode.nextElementSibling; + this.resizeListener.clear(); + const element = this.domNode; + if (element) { + element.parentElement?.removeChild(element); + this._notebookEditor.removeInset( + this._diffElementViewModel, + this._nestedCell, + viewModel, + this._diffSide + ); + } + + viewModel.pickedMimeType = pick; + this.render(index, nextElement as HTMLElement); + } + } + + private generateRendererInfo(renderId: string | undefined): string { + if (renderId === undefined || renderId === BUILTIN_RENDERER_ID) { + return nls.localize('builtinRenderInfo', "built-in"); + } + + const renderInfo = this._notebookService.getRendererInfo(renderId); + + if (renderInfo) { + const displayName = renderInfo.displayName !== '' ? renderInfo.displayName : renderInfo.id; + return `${displayName} (${renderInfo.extensionId.value})`; + } + + return nls.localize('builtinRenderInfo', "built-in"); + } + + getCellOutputCurrentIndex() { + return this._diffElementViewModel.getNestedCellViewModel(this._diffSide).outputs.indexOf(this.output.model); + } + + updateHeight(index: number, height: number) { + this._diffElementViewModel.updateOutputHeight(this._diffSide, index, height); + } + + getOutputOffsetInContainer(index: number) { + return this._diffElementViewModel.getOutputOffsetInContainer(this._diffSide, index); + } + + getOutputOffsetInCell(index: number) { + return this._diffElementViewModel.getOutputOffsetInCell(this._diffSide, index); + } +} + +export class OutputContainer extends Disposable { + private _outputEntries = new Map(); + constructor( + private _editor: INotebookTextDiffEditor, + private _notebookTextModel: NotebookTextModel, + private _diffElementViewModel: DiffElementViewModelBase, + private _nestedCellViewModel: DiffNestedCellViewModel, + private _diffSide: DiffSide, + private _outputContainer: HTMLElement, + @INotebookService private _notebookService: INotebookService, + @IQuickInputService private readonly _quickInputService: IQuickInputService, + @IOpenerService readonly _openerService: IOpenerService, + @ITextFileService readonly _textFileService: ITextFileService, + + ) { + super(); + this._register(this._diffElementViewModel.onDidLayoutChange(() => { + this._outputEntries.forEach((value, key) => { + const index = _nestedCellViewModel.outputs.indexOf(key.model); + if (index >= 0) { + const top = this._diffElementViewModel.getOutputOffsetInContainer(this._diffSide, index); + value.domNode.style.top = `${top}px`; + } + }); + })); + + this._register(this._nestedCellViewModel.textModel.onDidChangeOutputs(splices => { + this._updateOutputs(splices); + })); + } + + private _updateOutputs(splices: NotebookCellOutputsSplice[]) { + if (!splices.length) { + return; + } + + const removedKeys: ICellOutputViewModel[] = []; + + this._outputEntries.forEach((value, key) => { + if (this._nestedCellViewModel.outputsViewModels.indexOf(key) < 0) { + // already removed + removedKeys.push(key); + // remove element from DOM + this._outputContainer.removeChild(value.domNode); + if (key.isDisplayOutput()) { + this._editor.removeInset(this._diffElementViewModel, this._nestedCellViewModel, key, this._diffSide); + } + } + }); + + removedKeys.forEach(key => { + this._outputEntries.get(key)?.dispose(); + this._outputEntries.delete(key); + }); + + let prevElement: HTMLElement | undefined = undefined; + const outputsToRender = this._nestedCellViewModel.outputsViewModels; + + outputsToRender.reverse().forEach(output => { + if (this._outputEntries.has(output)) { + // already exist + prevElement = this._outputEntries.get(output)!.domNode; + return; + } + + // newly added element + const currIndex = this._nestedCellViewModel.outputsViewModels.indexOf(output); + this._renderOutput(output, currIndex, prevElement); + prevElement = this._outputEntries.get(output)?.domNode; + }); + } + render() { + // TODO, outputs to render (should have a limit) + for (let index = 0; index < this._nestedCellViewModel.outputsViewModels.length; index++) { + const currOutput = this._nestedCellViewModel.outputsViewModels[index]; + + // always add to the end + this._renderOutput(currOutput, index, undefined); + } + } + + showOutputs() { + for (let index = 0; index < this._nestedCellViewModel.outputsViewModels.length; index++) { + const currOutput = this._nestedCellViewModel.outputsViewModels[index]; + + if (currOutput.isDisplayOutput()) { + // always add to the end + this._editor.showInset(this._diffElementViewModel, currOutput.cellViewModel, currOutput, this._diffSide); + } + } + } + + hideOutputs() { + this._outputEntries.forEach((outputElement, cellOutputViewModel) => { + if (cellOutputViewModel.isDisplayOutput()) { + this._editor.hideInset(this._diffElementViewModel, this._nestedCellViewModel, cellOutputViewModel); + } + }); + } + + private _renderOutput(currOutput: ICellOutputViewModel, index: number, beforeElement?: HTMLElement) { + if (!this._outputEntries.has(currOutput)) { + this._outputEntries.set(currOutput, new OutputElement(this._editor, this._notebookTextModel, this._notebookService, this._quickInputService, this._diffElementViewModel, this._diffSide, this._nestedCellViewModel, this._outputContainer, currOutput)); + } + + const renderElement = this._outputEntries.get(currOutput)!; + renderElement.render(index, beforeElement); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts new file mode 100644 index 00000000..4d10ed22 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts @@ -0,0 +1,498 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { CellDiffViewModelLayoutChangeEvent, DiffSide, DIFF_CELL_MARGIN, IDiffElementLayoutInfo } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; +import { IGenericCellViewModel, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { hash } from 'vs/base/common/hash'; +import { format } from 'vs/base/common/jsonFormatter'; +import { applyEdits } from 'vs/base/common/jsonEdit'; +import { NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { DiffNestedCellViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel'; +import { URI } from 'vs/base/common/uri'; +import { NotebookDiffEditorEventDispatcher, NotebookDiffViewEventType } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher'; +import * as editorCommon from 'vs/editor/common/editorCommon'; + +export enum PropertyFoldingState { + Expanded, + Collapsed +} + +export const OUTPUT_EDITOR_HEIGHT_MAGIC = 1440; + +type ILayoutInfoDelta0 = { [K in keyof IDiffElementLayoutInfo]?: number; }; +interface ILayoutInfoDelta extends ILayoutInfoDelta0 { + rawOutputHeight?: number; + recomputeOutput?: boolean; +} + +export abstract class DiffElementViewModelBase extends Disposable { + public metadataFoldingState: PropertyFoldingState; + public outputFoldingState: PropertyFoldingState; + protected _layoutInfoEmitter = new Emitter(); + onDidLayoutChange = this._layoutInfoEmitter.event; + protected _stateChangeEmitter = new Emitter<{ renderOutput: boolean; }>(); + onDidStateChange = this._stateChangeEmitter.event; + protected _layoutInfo!: IDiffElementLayoutInfo; + + set rawOutputHeight(height: number) { + this._layout({ rawOutputHeight: Math.min(OUTPUT_EDITOR_HEIGHT_MAGIC, height) }); + } + + get rawOutputHeight() { + throw new Error('Use Cell.layoutInfo.rawOutputHeight'); + } + + set outputStatusHeight(height: number) { + this._layout({ outputStatusHeight: height }); + } + + get outputStatusHeight() { + throw new Error('Use Cell.layoutInfo.outputStatusHeight'); + } + + set editorHeight(height: number) { + this._layout({ editorHeight: height }); + } + + get editorHeight() { + throw new Error('Use Cell.layoutInfo.editorHeight'); + } + + set editorMargin(margin: number) { + this._layout({ editorMargin: margin }); + } + + get editorMargin() { + throw new Error('Use Cell.layoutInfo.editorMargin'); + } + + set metadataHeight(height: number) { + this._layout({ metadataHeight: height }); + } + + get metadataHeight() { + throw new Error('Use Cell.layoutInfo.metadataHeight'); + } + + private _renderOutput = true; + + set renderOutput(value: boolean) { + this._renderOutput = value; + this._layout({ recomputeOutput: true }); + this._stateChangeEmitter.fire({ renderOutput: this._renderOutput }); + } + + get renderOutput() { + return this._renderOutput; + } + + get layoutInfo(): IDiffElementLayoutInfo { + return this._layoutInfo; + } + + private _sourceEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null; + private _outputEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null; + private _metadataEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null; + + constructor( + readonly mainDocumentTextModel: NotebookTextModel, + readonly original: DiffNestedCellViewModel | undefined, + readonly modified: DiffNestedCellViewModel | undefined, + readonly type: 'unchanged' | 'insert' | 'delete' | 'modified', + readonly editorEventDispatcher: NotebookDiffEditorEventDispatcher + ) { + super(); + this._layoutInfo = { + width: 0, + editorHeight: 0, + editorMargin: 0, + metadataHeight: 0, + metadataStatusHeight: 25, + rawOutputHeight: 0, + outputTotalHeight: 0, + outputStatusHeight: 25, + bodyMargin: 32, + totalHeight: 82 + }; + + this.metadataFoldingState = PropertyFoldingState.Collapsed; + this.outputFoldingState = PropertyFoldingState.Collapsed; + + this._register(this.editorEventDispatcher.onDidChangeLayout(e => { + this._layoutInfoEmitter.fire({ outerWidth: true }); + })); + } + + layoutChange() { + this._layout({ recomputeOutput: true }); + } + + protected _layout(delta: ILayoutInfoDelta) { + const width = delta.width !== undefined ? delta.width : this._layoutInfo.width; + const editorHeight = delta.editorHeight !== undefined ? delta.editorHeight : this._layoutInfo.editorHeight; + const editorMargin = delta.editorMargin !== undefined ? delta.editorMargin : this._layoutInfo.editorMargin; + const metadataHeight = delta.metadataHeight !== undefined ? delta.metadataHeight : this._layoutInfo.metadataHeight; + const metadataStatusHeight = delta.metadataStatusHeight !== undefined ? delta.metadataStatusHeight : this._layoutInfo.metadataStatusHeight; + const rawOutputHeight = delta.rawOutputHeight !== undefined ? delta.rawOutputHeight : this._layoutInfo.rawOutputHeight; + const outputStatusHeight = delta.outputStatusHeight !== undefined ? delta.outputStatusHeight : this._layoutInfo.outputStatusHeight; + const bodyMargin = delta.bodyMargin !== undefined ? delta.bodyMargin : this._layoutInfo.bodyMargin; + const outputHeight = (delta.recomputeOutput || delta.rawOutputHeight !== undefined) ? this._getOutputTotalHeight(rawOutputHeight) : this._layoutInfo.outputTotalHeight; + + const totalHeight = editorHeight + + editorMargin + + metadataHeight + + metadataStatusHeight + + outputHeight + + outputStatusHeight + + bodyMargin; + + const newLayout: IDiffElementLayoutInfo = { + width: width, + editorHeight: editorHeight, + editorMargin: editorMargin, + metadataHeight: metadataHeight, + metadataStatusHeight: metadataStatusHeight, + outputTotalHeight: outputHeight, + outputStatusHeight: outputStatusHeight, + bodyMargin: bodyMargin, + rawOutputHeight: rawOutputHeight, + totalHeight: totalHeight + }; + + const changeEvent: CellDiffViewModelLayoutChangeEvent = {}; + + if (newLayout.width !== this._layoutInfo.width) { + changeEvent.width = true; + } + + if (newLayout.editorHeight !== this._layoutInfo.editorHeight) { + changeEvent.editorHeight = true; + } + + if (newLayout.editorMargin !== this._layoutInfo.editorMargin) { + changeEvent.editorMargin = true; + } + + if (newLayout.metadataHeight !== this._layoutInfo.metadataHeight) { + changeEvent.metadataHeight = true; + } + + if (newLayout.metadataStatusHeight !== this._layoutInfo.metadataStatusHeight) { + changeEvent.metadataStatusHeight = true; + } + + if (newLayout.outputTotalHeight !== this._layoutInfo.outputTotalHeight) { + changeEvent.outputTotalHeight = true; + } + + if (newLayout.outputStatusHeight !== this._layoutInfo.outputStatusHeight) { + changeEvent.outputStatusHeight = true; + } + + if (newLayout.bodyMargin !== this._layoutInfo.bodyMargin) { + changeEvent.bodyMargin = true; + } + + if (newLayout.totalHeight !== this._layoutInfo.totalHeight) { + changeEvent.totalHeight = true; + } + + this._layoutInfo = newLayout; + this._fireLayoutChangeEvent(changeEvent); + } + + private _getOutputTotalHeight(rawOutputHeight: number) { + if (this.outputFoldingState === PropertyFoldingState.Collapsed) { + return 0; + } + + if (this.renderOutput) { + if (this.isOutputEmpty()) { + // single line; + return 24; + } + return this.getRichOutputTotalHeight(); + } else { + return rawOutputHeight; + } + } + + private _fireLayoutChangeEvent(state: CellDiffViewModelLayoutChangeEvent) { + this._layoutInfoEmitter.fire(state); + this.editorEventDispatcher.emit([{ type: NotebookDiffViewEventType.CellLayoutChanged, source: this._layoutInfo }]); + } + + abstract checkIfOutputsModified(): boolean; + abstract checkMetadataIfModified(): boolean; + abstract isOutputEmpty(): boolean; + abstract getRichOutputTotalHeight(): number; + abstract getCellByUri(cellUri: URI): IGenericCellViewModel; + abstract getOutputOffsetInCell(diffSide: DiffSide, index: number): number; + abstract getOutputOffsetInContainer(diffSide: DiffSide, index: number): number; + abstract updateOutputHeight(diffSide: DiffSide, index: number, height: number): void; + abstract getNestedCellViewModel(diffSide: DiffSide): DiffNestedCellViewModel; + + getComputedCellContainerWidth(layoutInfo: NotebookLayoutInfo, diffEditor: boolean, fullWidth: boolean) { + if (fullWidth) { + return layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0) - 2; + } + + return (layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)) / 2 - 18 - 2; + } + + getOutputEditorViewState(): editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null { + return this._outputEditorViewState; + } + + saveOutputEditorViewState(viewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null) { + this._outputEditorViewState = viewState; + } + + getMetadataEditorViewState(): editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null { + return this._metadataEditorViewState; + } + + saveMetadataEditorViewState(viewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null) { + this._metadataEditorViewState = viewState; + } + + getSourceEditorViewState(): editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null { + return this._sourceEditorViewState; + } + + saveSpirceEditorViewState(viewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null) { + this._sourceEditorViewState = viewState; + } +} + +export class SideBySideDiffElementViewModel extends DiffElementViewModelBase { + get originalDocument() { + return this.otherDocumentTextModel; + } + + get modifiedDocument() { + return this.mainDocumentTextModel; + } + + constructor( + readonly mainDocumentTextModel: NotebookTextModel, + readonly otherDocumentTextModel: NotebookTextModel, + readonly original: DiffNestedCellViewModel, + readonly modified: DiffNestedCellViewModel, + readonly type: 'unchanged' | 'modified', + readonly editorEventDispatcher: NotebookDiffEditorEventDispatcher + ) { + super( + mainDocumentTextModel, + original, + modified, + type, + editorEventDispatcher); + + this.metadataFoldingState = PropertyFoldingState.Collapsed; + this.outputFoldingState = PropertyFoldingState.Collapsed; + + if (this.checkMetadataIfModified()) { + this.metadataFoldingState = PropertyFoldingState.Expanded; + } + + if (this.checkIfOutputsModified()) { + this.outputFoldingState = PropertyFoldingState.Expanded; + } + + this._register(this.original.onDidChangeOutputLayout(() => { + this._layout({ recomputeOutput: true }); + })); + + this._register(this.modified.onDidChangeOutputLayout(() => { + this._layout({ recomputeOutput: true }); + })); + } + + checkIfOutputsModified() { + return !this.mainDocumentTextModel.transientOptions.transientOutputs && hash(this.original?.outputs ?? []) !== hash(this.modified?.outputs ?? []); + } + + checkMetadataIfModified(): boolean { + return hash(getFormatedMetadataJSON(this.mainDocumentTextModel, this.original?.metadata || {}, this.original?.language)) !== hash(getFormatedMetadataJSON(this.mainDocumentTextModel, this.modified?.metadata ?? {}, this.modified?.language)); + } + + updateOutputHeight(diffSide: DiffSide, index: number, height: number) { + if (diffSide === DiffSide.Original) { + this.original.updateOutputHeight(index, height); + } else { + this.modified.updateOutputHeight(index, height); + } + } + + getOutputOffsetInContainer(diffSide: DiffSide, index: number) { + if (diffSide === DiffSide.Original) { + return this.original.getOutputOffset(index); + } else { + return this.modified.getOutputOffset(index); + } + } + + getOutputOffsetInCell(diffSide: DiffSide, index: number) { + const offsetInOutputsContainer = this.getOutputOffsetInContainer(diffSide, index); + + return this._layoutInfo.editorHeight + + this._layoutInfo.editorMargin + + this._layoutInfo.metadataHeight + + this._layoutInfo.metadataStatusHeight + + this._layoutInfo.outputStatusHeight + + this._layoutInfo.bodyMargin / 2 + + offsetInOutputsContainer; + } + + isOutputEmpty() { + if (this.mainDocumentTextModel.transientOptions.transientOutputs) { + return true; + } + + if (this.checkIfOutputsModified()) { + return false; + } + + // outputs are not changed + + return (this.original?.outputs || []).length === 0; + } + + getRichOutputTotalHeight() { + return Math.max(this.original.getOutputTotalHeight(), this.modified.getOutputTotalHeight()); + } + + getNestedCellViewModel(diffSide: DiffSide): DiffNestedCellViewModel { + throw new Error('Method not implemented.'); + } + + getCellByUri(cellUri: URI): IGenericCellViewModel { + if (cellUri.toString() === this.original.uri.toString()) { + return this.original; + } else { + return this.modified; + } + } +} + +export class SingleSideDiffElementViewModel extends DiffElementViewModelBase { + get cellViewModel() { + return this.type === 'insert' ? this.modified! : this.original!; + } + + get originalDocument() { + if (this.type === 'insert') { + return this.otherDocumentTextModel; + } else { + return this.mainDocumentTextModel; + } + } + + get modifiedDocument() { + if (this.type === 'insert') { + return this.mainDocumentTextModel; + } else { + return this.otherDocumentTextModel; + } + } + + constructor( + readonly mainDocumentTextModel: NotebookTextModel, + readonly otherDocumentTextModel: NotebookTextModel, + readonly original: DiffNestedCellViewModel | undefined, + readonly modified: DiffNestedCellViewModel | undefined, + readonly type: 'insert' | 'delete', + readonly editorEventDispatcher: NotebookDiffEditorEventDispatcher + ) { + super(mainDocumentTextModel, original, modified, type, editorEventDispatcher); + this._register(this.cellViewModel!.onDidChangeOutputLayout(() => { + this._layout({ recomputeOutput: true }); + })); + } + + getNestedCellViewModel(diffSide: DiffSide): DiffNestedCellViewModel { + return this.type === 'insert' ? this.modified! : this.original!; + } + + + checkIfOutputsModified(): boolean { + return false; + } + + checkMetadataIfModified(): boolean { + return false; + } + + updateOutputHeight(diffSide: DiffSide, index: number, height: number) { + this.cellViewModel?.updateOutputHeight(index, height); + } + + getOutputOffsetInContainer(diffSide: DiffSide, index: number) { + return this.cellViewModel!.getOutputOffset(index); + } + + getOutputOffsetInCell(diffSide: DiffSide, index: number) { + const offsetInOutputsContainer = this.cellViewModel!.getOutputOffset(index); + + return this._layoutInfo.editorHeight + + this._layoutInfo.editorMargin + + this._layoutInfo.metadataHeight + + this._layoutInfo.metadataStatusHeight + + this._layoutInfo.outputStatusHeight + + this._layoutInfo.bodyMargin / 2 + + offsetInOutputsContainer; + } + + isOutputEmpty() { + if (this.mainDocumentTextModel.transientOptions.transientOutputs) { + return true; + } + + // outputs are not changed + + return (this.original?.outputs || this.modified?.outputs || []).length === 0; + } + + getRichOutputTotalHeight() { + return this.cellViewModel?.getOutputTotalHeight() ?? 0; + } + + getCellByUri(cellUri: URI): IGenericCellViewModel { + return this.cellViewModel!; + } +} + +export function getFormatedMetadataJSON(documentTextModel: NotebookTextModel, metadata: NotebookCellMetadata, language?: string) { + let filteredMetadata: { [key: string]: any } = {}; + + if (documentTextModel) { + const transientMetadata = documentTextModel.transientOptions.transientMetadata; + + const keys = new Set([...Object.keys(metadata)]); + for (let key of keys) { + if (!(transientMetadata[key as keyof NotebookCellMetadata]) + ) { + filteredMetadata[key] = metadata[key as keyof NotebookCellMetadata]; + } + } + } else { + filteredMetadata = metadata; + } + + const content = JSON.stringify({ + language, + ...filteredMetadata + }); + + const edits = format(content, undefined, {}); + const metadataSource = applyEdits(content, edits); + + return metadataSource; +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts new file mode 100644 index 00000000..bd9f8d2a --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts @@ -0,0 +1,123 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { generateUuid } from 'vs/base/common/uuid'; +import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; +import { IDiffNestedCellViewModel } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; +import { CellViewModelStateChangeEvent, ICellOutputViewModel, IGenericCellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellOutputViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/cellOutputViewModel'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; + +export class DiffNestedCellViewModel extends Disposable implements IDiffNestedCellViewModel, IGenericCellViewModel { + private _id: string; + get id() { + return this._id; + } + + get outputs() { + return this.textModel.outputs; + } + + get language() { + return this.textModel.language; + } + + get metadata() { + return this.textModel.metadata; + } + + get uri() { + return this.textModel.uri; + } + + get handle() { + return this.textModel.handle; + } + + protected readonly _onDidChangeState: Emitter = this._register(new Emitter()); + + private _hoveringOutput: boolean = false; + public get outputIsHovered(): boolean { + return this._hoveringOutput; + } + + public set outputIsHovered(v: boolean) { + this._hoveringOutput = v; + this._onDidChangeState.fire({ outputIsHoveredChanged: true }); + } + private _outputViewModels: ICellOutputViewModel[]; + + get outputsViewModels() { + return this._outputViewModels; + } + + protected _outputCollection: number[] = []; + protected _outputsTop: PrefixSumComputer | null = null; + protected readonly _onDidChangeOutputLayout = new Emitter(); + readonly onDidChangeOutputLayout = this._onDidChangeOutputLayout.event; + + + constructor( + readonly textModel: NotebookCellTextModel, + @INotebookService private _notebookService: INotebookService + ) { + super(); + this._id = generateUuid(); + + this._outputViewModels = this.textModel.outputs.map(output => new CellOutputViewModel(this, output, this._notebookService)); + this._register(this.textModel.onDidChangeOutputs((splices) => { + splices.reverse().forEach(splice => { + this._outputCollection.splice(splice[0], splice[1], ...splice[2].map(() => 0)); + this._outputViewModels.splice(splice[0], splice[1], ...splice[2].map(output => new CellOutputViewModel(this, output, this._notebookService))); + }); + + this._outputsTop = null; + this._onDidChangeOutputLayout.fire(); + })); + this._outputCollection = new Array(this.textModel.outputs.length); + } + + private _ensureOutputsTop() { + if (!this._outputsTop) { + const values = new Uint32Array(this._outputCollection.length); + for (let i = 0; i < this._outputCollection.length; i++) { + values[i] = this._outputCollection[i]; + } + + this._outputsTop = new PrefixSumComputer(values); + } + } + + getOutputOffset(index: number): number { + this._ensureOutputsTop(); + + if (index >= this._outputCollection.length) { + throw new Error('Output index out of range!'); + } + + return this._outputsTop!.getAccumulatedValue(index - 1); + } + + updateOutputHeight(index: number, height: number): void { + if (index >= this._outputCollection.length) { + throw new Error('Output index out of range!'); + } + + this._ensureOutputsTop(); + this._outputCollection[index] = height; + if (this._outputsTop!.changeValue(index, height)) { + this._onDidChangeOutputLayout.fire(); + } + } + + getOutputTotalHeight() { + this._ensureOutputsTop(); + + return this._outputsTop?.getTotalValue() ?? 0; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/eventDispatcher.ts b/src/vs/workbench/contrib/notebook/browser/diff/eventDispatcher.ts new file mode 100644 index 00000000..2e4cb22e --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/eventDispatcher.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { IDiffElementLayoutInfo } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; +import { NotebookLayoutChangeEvent, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; + +export enum NotebookDiffViewEventType { + LayoutChanged = 1, + CellLayoutChanged = 2 + // MetadataChanged = 2, + // CellStateChanged = 3 +} + +export class NotebookDiffLayoutChangedEvent { + public readonly type = NotebookDiffViewEventType.LayoutChanged; + + constructor(readonly source: NotebookLayoutChangeEvent, readonly value: NotebookLayoutInfo) { + + } +} + +export class NotebookCellLayoutChangedEvent { + public readonly type = NotebookDiffViewEventType.CellLayoutChanged; + + constructor(readonly source: IDiffElementLayoutInfo) { + + } +} + +export type NotebookDiffViewEvent = NotebookDiffLayoutChangedEvent | NotebookCellLayoutChangedEvent; + +export class NotebookDiffEditorEventDispatcher { + protected readonly _onDidChangeLayout = new Emitter(); + readonly onDidChangeLayout = this._onDidChangeLayout.event; + protected readonly _onDidChangeCellLayout = new Emitter(); + readonly onDidChangeCellLayout = this._onDidChangeCellLayout.event; + + constructor() { + } + + emit(events: NotebookDiffViewEvent[]) { + for (let i = 0, len = events.length; i < len; i++) { + const e = events[i]; + + switch (e.type) { + case NotebookDiffViewEventType.LayoutChanged: + this._onDidChangeLayout.fire(e); + break; + case NotebookDiffViewEventType.CellLayoutChanged: + this._onDidChangeCellLayout.fire(e); + break; + } + } + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css index 950cbce2..40746544 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css @@ -21,6 +21,31 @@ flex-direction: row; } +.notebook-text-diff-editor .webview-cover { + user-select: initial; + -webkit-user-select: initial; +} + +.notebook-text-diff-editor .cell-body .border-container { + position: absolute; + width: calc(100% - 32px); +} + +.notebook-text-diff-editor .cell-body .border-container .top-border, +.notebook-text-diff-editor .cell-body .border-container .bottom-border { + position: absolute; + width: 100%; +} + +.notebook-text-diff-editor .cell-body .border-container .left-border, +.notebook-text-diff-editor .cell-body .border-container .right-border { + position: absolute; +} + +.notebook-text-diff-editor .cell-body .border-container .right-border { + left: 100%; +} + .notebook-text-diff-editor .cell-body.right { flex-direction: row-reverse; } @@ -32,18 +57,20 @@ .notebook-text-diff-editor .cell-body .cell-diff-editor-container { width: 100%; - overflow: hidden; + /* why we overflow hidden at the beginning?*/ + /* overflow: hidden; */ } .notebook-text-diff-editor .cell-body .cell-diff-editor-container .metadata-editor-container.diff, .notebook-text-diff-editor .cell-body .cell-diff-editor-container .output-editor-container.diff, .notebook-text-diff-editor .cell-body .cell-diff-editor-container .editor-container.diff { /** 100% + diffOverviewWidth */ - width: calc(100% + 30px); + width: calc(100%); } .notebook-text-diff-editor .cell-body .cell-diff-editor-container .metadata-editor-container .monaco-diff-editor .diffOverview, -.notebook-text-diff-editor .cell-body .cell-diff-editor-container .editor-container.diff .monaco-diff-editor .diffOverview { +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .editor-container.diff .monaco-diff-editor .diffOverview, +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .output-editor-container.diff .monaco-diff-editor .diffOverview { display: none; } @@ -106,10 +133,15 @@ overflow: hidden; } +.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row { + overflow: visible !important; +} + .monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row, .monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover, .monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { outline: none !important; + background-color: transparent !important; } .notebook-text-diff-editor .cell-diff-editor-container .editor-input-toolbar-container { @@ -118,3 +150,120 @@ top: 16px; margin: 4px 2px; } + +.monaco-workbench .notebook-text-diff-editor .cell-body { + height: 0; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body .output-view-container { + user-select: text; + -webkit-user-select: text; + -ms-user-select: text; + white-space: initial; + cursor: auto; + position: relative; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body .output-view-container .output-plaintext { + white-space: pre; + overflow-x: hidden; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.left .output-view-container .output-inner-container, +.monaco-workbench .notebook-text-diff-editor .cell-body.right .output-view-container .output-inner-container { + width: 100%; + padding: 0px 8px; + box-sizing: border-box; + overflow-x: hidden; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.left .output-view-container .output-inner-container { + padding: 0px 8px 0px 32px; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.right .output-view-container .output-inner-container { + padding: 0px 8px 0px 32px; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.full .output-view-container .output-inner-container { + width: 100%; + padding: 4px 8px 4px 32px; + box-sizing: border-box; + overflow-x: hidden; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-left { + top: 0; + position: absolute; + left: 0; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-right { + position: absolute; + top: 0; + left: 50%; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-left, +.monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-right { + width: 50%; + display: inline-block; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-left div.foreground, +.monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-right div.foreground { + width: 100%; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container > div.foreground { + width: 100%; + min-height: 24px; + box-sizing: border-box; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .error_message { + color: red; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .error > div { + white-space: normal; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .error pre.traceback { + box-sizing: border-box; + padding: 8px 0; + margin: 0px; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .error .traceback > span { + display: block; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .display img { + max-width: 100%; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .multi-mimetype-output { + position: absolute; + top: 4px; + left: 8px; + width: 16px; + height: 16px; + cursor: pointer; + padding: 2px 4px 4px 2px; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .output-empty-view span { + opacity: 0.7; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .output-empty-view { + font-style: italic; + height: 24px; + margin: auto; + padding-left: 12px; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container pre { + margin: 4px 0; +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts index cb8709d1..0911630f 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts @@ -8,10 +8,11 @@ import { localize } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ActiveEditorContext, viewColumnToEditorGroup } from 'vs/workbench/common/editor'; -import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; +import { DiffElementViewModelBase } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { NOTEBOOK_DIFF_CELL_PROPERTY, NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; import { NotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor'; import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookDiffEditorInput'; -import { openAsTextIcon, revertIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { openAsTextIcon, renderOutputIcon, revertIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -60,12 +61,14 @@ registerAction2(class extends Action2 { icon: revertIcon, f1: false, menu: { - id: MenuId.NotebookDiffCellMetadataTitle - } + id: MenuId.NotebookDiffCellMetadataTitle, + when: NOTEBOOK_DIFF_CELL_PROPERTY + }, + precondition: NOTEBOOK_DIFF_CELL_PROPERTY } ); } - run(accessor: ServicesAccessor, context?: { cell: CellDiffViewModel }) { + run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { if (!context) { return; } @@ -77,7 +80,55 @@ registerAction2(class extends Action2 { return; } - modified.metadata = original.metadata; + modified.textModel.metadata = original.metadata; + } +}); + +// registerAction2(class extends Action2 { +// constructor() { +// super( +// { +// id: 'notebook.diff.cell.switchOutputRenderingStyle', +// title: localize('notebook.diff.cell.switchOutputRenderingStyle', "Switch Outputs Rendering"), +// icon: renderOutputIcon, +// f1: false, +// menu: { +// id: MenuId.NotebookDiffCellOutputsTitle +// } +// } +// ); +// } +// run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { +// if (!context) { +// return; +// } + +// context.cell.renderOutput = true; +// } +// }); + + +registerAction2(class extends Action2 { + constructor() { + super( + { + id: 'notebook.diff.cell.switchOutputRenderingStyleToText', + title: localize('notebook.diff.cell.switchOutputRenderingStyleToText', "Switch Output Rendering"), + icon: renderOutputIcon, + f1: false, + menu: { + id: MenuId.NotebookDiffCellOutputsTitle, + when: NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED + } + } + ); + } + run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { + if (!context) { + return; + } + + context.cell.renderOutput = !context.cell.renderOutput; } }); @@ -90,12 +141,14 @@ registerAction2(class extends Action2 { icon: revertIcon, f1: false, menu: { - id: MenuId.NotebookDiffCellOutputsTitle - } + id: MenuId.NotebookDiffCellOutputsTitle, + when: NOTEBOOK_DIFF_CELL_PROPERTY + }, + precondition: NOTEBOOK_DIFF_CELL_PROPERTY } ); } - run(accessor: ServicesAccessor, context?: { cell: CellDiffViewModel }) { + run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { if (!context) { return; } @@ -107,10 +160,11 @@ registerAction2(class extends Action2 { return; } - modified.spliceNotebookCellOutputs([[0, modified.outputs.length, original.outputs]]); + modified.textModel.spliceNotebookCellOutputs([[0, modified.outputs.length, original.outputs]]); } }); + registerAction2(class extends Action2 { constructor() { super( @@ -120,12 +174,15 @@ registerAction2(class extends Action2 { icon: revertIcon, f1: false, menu: { - id: MenuId.NotebookDiffCellInputTitle - } + id: MenuId.NotebookDiffCellInputTitle, + when: NOTEBOOK_DIFF_CELL_PROPERTY + }, + precondition: NOTEBOOK_DIFF_CELL_PROPERTY + } ); } - run(accessor: ServicesAccessor, context?: { cell: CellDiffViewModel }) { + run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { if (!context) { return; } @@ -139,7 +196,7 @@ registerAction2(class extends Action2 { const bulkEditService = accessor.get(IBulkEditService); return bulkEditService.apply([ - new ResourceTextEdit(modified.uri, { range: modified.getFullModelRange(), text: original.getValue() }), + new ResourceTextEdit(modified.uri, { range: modified.textModel.getFullModelRange(), text: original.textModel.getValue() }), ], { quotableLabel: 'Split Notebook Cell' }); } }); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts new file mode 100644 index 00000000..611b0f26 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ICommonCellInfo, ICommonNotebookEditor, IDisplayOutputViewModel, IGenericCellViewModel, IInsetRenderOutput, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { DiffElementViewModelBase } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { Event } from 'vs/base/common/event'; +import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + +export enum DiffSide { + Original = 0, + Modified = 1 +} + +export interface IDiffCellInfo extends ICommonCellInfo { + diffElement: DiffElementViewModelBase; +} + +export interface INotebookTextDiffEditor extends ICommonNotebookEditor { + readonly textModel?: NotebookTextModel; + onMouseUp: Event<{ readonly event: MouseEvent; readonly target: DiffElementViewModelBase; }>; + onDidDynamicOutputRendered: Event<{ cell: IGenericCellViewModel, output: IDisplayOutputViewModel }>; + getOverflowContainerDomNode(): HTMLElement; + getLayoutInfo(): NotebookLayoutInfo; + layoutNotebookCell(cell: DiffElementViewModelBase, height: number): void; + getOutputRenderer(): OutputRenderer; + createInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: IDiffNestedCellViewModel, output: IInsetRenderOutput, getOffset: () => number, diffSide: DiffSide): void; + showInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: IDiffNestedCellViewModel, displayOutput: IDisplayOutputViewModel, diffSide: DiffSide): void; + removeInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: IDiffNestedCellViewModel, output: IDisplayOutputViewModel, diffSide: DiffSide): void; + hideInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: IDiffNestedCellViewModel, output: IDisplayOutputViewModel): void; + /** + * Trigger the editor to scroll from scroll event programmatically + */ + triggerScroll(event: IMouseWheelEvent): void; + getCellByInfo(cellInfo: ICommonCellInfo): IGenericCellViewModel; + focusNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): void; + focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): void; + updateOutputHeight(cellInfo: ICommonCellInfo, output: IDisplayOutputViewModel, height: number, isInit: boolean): void; + deltaCellOutputContainerClassNames(diffSide: DiffSide, cellId: string, added: string[], removed: string[]): void; +} + +export interface IDiffNestedCellViewModel { + +} + +export interface CellDiffCommonRenderTemplate { + readonly leftBorder: HTMLElement; + readonly rightBorder: HTMLElement; + readonly topBorder: HTMLElement; + readonly bottomBorder: HTMLElement; +} + +export interface CellDiffSingleSideRenderTemplate extends CellDiffCommonRenderTemplate { + readonly container: HTMLElement; + readonly body: HTMLElement; + readonly diffEditorContainer: HTMLElement; + readonly diagonalFill: HTMLElement; + readonly elementDisposables: DisposableStore; + readonly sourceEditor: CodeEditorWidget; + readonly metadataHeaderContainer: HTMLElement; + readonly metadataInfoContainer: HTMLElement; + readonly outputHeaderContainer: HTMLElement; + readonly outputInfoContainer: HTMLElement; + +} + + +export interface CellDiffSideBySideRenderTemplate extends CellDiffCommonRenderTemplate { + readonly container: HTMLElement; + readonly body: HTMLElement; + readonly diffEditorContainer: HTMLElement; + readonly elementDisposables: DisposableStore; + readonly sourceEditor: DiffEditorWidget; + readonly editorContainer: HTMLElement; + readonly inputToolbarContainer: HTMLElement; + readonly toolbar: ToolBar; + readonly metadataHeaderContainer: HTMLElement; + readonly metadataInfoContainer: HTMLElement; + readonly outputHeaderContainer: HTMLElement; + readonly outputInfoContainer: HTMLElement; +} + +export interface IDiffElementLayoutInfo { + totalHeight: number; + width: number; + editorHeight: number; + editorMargin: number; + metadataHeight: number; + metadataStatusHeight: number; + rawOutputHeight: number; + outputTotalHeight: number; + outputStatusHeight: number; + bodyMargin: number +} + +type IDiffElementSelfLayoutChangeEvent = { [K in keyof IDiffElementLayoutInfo]?: boolean }; + +export interface CellDiffViewModelLayoutChangeEvent extends IDiffElementSelfLayoutChangeEvent { + font?: BareFontInfo; + outerWidth?: boolean; + metadataEditor?: boolean; + outputEditor?: boolean; + outputView?: boolean; +} + +export const DIFF_CELL_MARGIN = 16; +export const NOTEBOOK_DIFF_CELL_PROPERTY = new RawContextKey('notebookDiffCellPropertyChanged', false); +export const NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED = new RawContextKey('notebookDiffCellPropertyExpanded', false); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts index 13f6973c..19d4f07e 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -13,32 +13,38 @@ import { notebookCellBorder, NotebookEditorWidget } from 'vs/workbench/contrib/n import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { NotebookDiffEditorInput } from '../notebookDiffEditorInput'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { WorkbenchList } from 'vs/platform/list/browser/listService'; -import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; +import { DiffElementViewModelBase, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { CellDiffRenderer, NotebookCellTextDiffListDelegate, NotebookTextDiffList } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList'; -import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { CellDiffSideBySideRenderer, CellDiffSingleSideRenderer, NotebookCellTextDiffListDelegate, NotebookTextDiffList } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { diffDiagonalFill, diffInserted, diffRemoved, editorBackground, focusBorder, foreground } from 'vs/platform/theme/common/colorRegistry'; import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; -import { getZoomLevel } from 'vs/base/browser/browser'; -import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { DIFF_CELL_MARGIN, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/common'; +import { getPixelRatio, getZoomLevel } from 'vs/base/browser/browser'; +import { IDisplayOutputLayoutUpdateRequest, IDisplayOutputViewModel, IGenericCellViewModel, IInsetRenderOutput, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { DiffSide, DIFF_CELL_MARGIN, IDiffCellInfo, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; import { Emitter } from 'vs/base/common/event'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { NotebookDiffEditorEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { INotebookDiffEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellUri, INotebookDiffEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { FileService } from 'vs/platform/files/common/fileService'; import { IFileService } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { IDiffChange } from 'vs/base/common/diff/diff'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; +import { SequencerByKey } from 'vs/base/common/async'; +import { generateUuid } from 'vs/base/common/uuid'; +import { IMouseWheelEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { DiffNestedCellViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel'; +import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; +import { CELL_OUTPUT_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { NotebookDiffEditorEventDispatcher, NotebookDiffLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher'; -export const IN_NOTEBOOK_TEXT_DIFF_EDITOR = new RawContextKey('isInNotebookTextDiffEditor', false); +const $ = DOM.$; export class NotebookTextDiffEditor extends EditorPane implements INotebookTextDiffEditor { static readonly ID: string = 'workbench.editor.notebookTextDiffEditor'; @@ -46,21 +52,38 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD private _rootElement!: HTMLElement; private _overflowContainer!: HTMLElement; private _dimension: DOM.Dimension | null = null; - private _list!: WorkbenchList; + private _diffElementViewModels: DiffElementViewModelBase[] = []; + private _list!: NotebookTextDiffList; + private _modifiedWebview: BackLayerWebView | null = null; + private _originalWebview: BackLayerWebView | null = null; + private _webviewTransparentCover: HTMLElement | null = null; private _fontInfo: BareFontInfo | undefined; - private readonly _onMouseUp = this._register(new Emitter<{ readonly event: MouseEvent; readonly target: CellDiffViewModel; }>()); + private readonly _onMouseUp = this._register(new Emitter<{ readonly event: MouseEvent; readonly target: DiffElementViewModelBase; }>()); public readonly onMouseUp = this._onMouseUp.event; private _eventDispatcher: NotebookDiffEditorEventDispatcher | undefined; protected _scopeContextKeyService!: IContextKeyService; private _model: INotebookDiffEditorModel | null = null; private _modifiedResourceDisposableStore = new DisposableStore(); + private _outputRenderer: OutputRenderer; get textModel() { return this._model?.modified.notebook; } private _revealFirst: boolean; + private readonly _insetModifyQueueByOutputId = new SequencerByKey(); + + protected _onDidDynamicOutputRendered = new Emitter<{ cell: IGenericCellViewModel, output: IDisplayOutputViewModel }>(); + onDidDynamicOutputRendered = this._onDidDynamicOutputRendered.event; + + private _localStore: DisposableStore = this._register(new DisposableStore()); + + private _isDisposed: boolean = false; + + get isDisposed() { + return this._isDisposed; + } constructor( @IInstantiationService readonly instantiationService: IInstantiationService, @@ -75,10 +98,40 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD ) { super(NotebookTextDiffEditor.ID, telemetryService, themeService, storageService); const editorOptions = this.configurationService.getValue('editor'); - this._fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()); + this._fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel(), getPixelRatio()); this._revealFirst = true; this._register(this._modifiedResourceDisposableStore); + this._outputRenderer = new OutputRenderer(this, this.instantiationService); + } + + focusNotebookCell(cell: IGenericCellViewModel, focus: 'output' | 'editor' | 'container'): void { + // throw new Error('Method not implemented.'); + } + + focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'output' | 'editor' | 'container'): void { + // throw new Error('Method not implemented.'); + } + + updateOutputHeight(cellInfo: IDiffCellInfo, output: IDisplayOutputViewModel, outputHeight: number, isInit: boolean): void { + const diffElement = cellInfo.diffElement; + const cell = this.getCellByInfo(cellInfo); + const outputIndex = cell.outputsViewModels.indexOf(output); + + if (diffElement instanceof SideBySideDiffElementViewModel) { + const info = CellUri.parse(cellInfo.cellUri); + if (!info) { + return; + } + + diffElement.updateOutputHeight(info.notebook.toString() === this._model?.original.resource.toString() ? DiffSide.Original : DiffSide.Modified, outputIndex, outputHeight); + } else { + diffElement.updateOutputHeight(diffElement.type === 'insert' ? DiffSide.Modified : DiffSide.Original, outputIndex, outputHeight); + } + + if (isInit) { + this._onDidDynamicOutputRendered.fire({ cell, output }); + } } protected createEditor(parent: HTMLElement): void { @@ -87,16 +140,17 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._overflowContainer.classList.add('notebook-overflow-widget-container', 'monaco-editor'); DOM.append(parent, this._overflowContainer); - const renderer = this.instantiationService.createInstance(CellDiffRenderer, this); + const renderers = [ + this.instantiationService.createInstance(CellDiffSingleSideRenderer, this), + this.instantiationService.createInstance(CellDiffSideBySideRenderer, this), + ]; this._list = this.instantiationService.createInstance( NotebookTextDiffList, 'NotebookTextDiff', this._rootElement, this.instantiationService.createInstance(NotebookCellTextDiffListDelegate), - [ - renderer - ], + renderers, this.contextKeyService, { setRowLineHeight: false, @@ -140,17 +194,94 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } ); + this._register(this._list); + this._register(this._list.onMouseUp(e => { if (e.element) { this._onMouseUp.fire({ event: e.browserEvent, target: e.element }); } })); + + // transparent cover + this._webviewTransparentCover = DOM.append(this._list.rowsContainer, $('.webview-cover')); + this._webviewTransparentCover.style.display = 'none'; + + this._register(DOM.addStandardDisposableGenericMouseDownListner(this._overflowContainer, (e: StandardMouseEvent) => { + if (e.target.classList.contains('slider') && this._webviewTransparentCover) { + this._webviewTransparentCover.style.display = 'block'; + } + })); + + this._register(DOM.addStandardDisposableGenericMouseUpListner(this._overflowContainer, () => { + if (this._webviewTransparentCover) { + // no matter when + this._webviewTransparentCover.style.display = 'none'; + } + })); + + this._register(this._list.onDidScroll(e => { + this._webviewTransparentCover!.style.top = `${e.scrollTop}px`; + })); + + + } + + private _updateOutputsOffsetsInWebview(scrollTop: number, scrollHeight: number, activeWebview: BackLayerWebView, getActiveNestedCell: (diffElement: DiffElementViewModelBase) => DiffNestedCellViewModel | undefined, diffSide: DiffSide) { + activeWebview.element.style.height = `${scrollHeight}px`; + + if (activeWebview.insetMapping) { + const updateItems: IDisplayOutputLayoutUpdateRequest[] = []; + const removedItems: IDisplayOutputViewModel[] = []; + activeWebview.insetMapping.forEach((value, key) => { + const cell = getActiveNestedCell(value.cellInfo.diffElement); + if (!cell) { + return; + } + + const viewIndex = this._list.indexOf(value.cellInfo.diffElement); + + if (viewIndex === undefined) { + return; + } + + if (cell.outputsViewModels.indexOf(key) < 0) { + // output is already gone + removedItems.push(key); + } else { + const cellTop = this._list.getAbsoluteTopOfElement(value.cellInfo.diffElement); + if (activeWebview.shouldUpdateInset(cell, key, cellTop)) { + const outputIndex = cell.outputsViewModels.indexOf(key); + const outputOffset = cellTop + value.cellInfo.diffElement.getOutputOffsetInCell(diffSide, outputIndex); + + updateItems.push({ + output: key, + cellTop: cellTop, + outputOffset: outputOffset + }); + } + } + + }); + + removedItems.forEach(output => activeWebview.removeInset(output)); + + if (updateItems.length) { + activeWebview.updateViewScrollTop(-scrollTop, false, updateItems); + } + } } async setInput(input: NotebookDiffEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { await super.setInput(input, options, context, token); - this._model = await input.resolve(); + const model = await input.resolve(); + if (this._model !== model) { + this._detachModel(); + this._model = model; + this._attachModel(); + } + + this._model = model; if (this._model === null) { return; } @@ -196,11 +327,73 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } })); - - this._eventDispatcher = new NotebookDiffEditorEventDispatcher(); + await this._createOriginalWebview(generateUuid(), this._model.original.resource); + await this._createModifiedWebview(generateUuid(), this._model.modified.resource); await this.updateLayout(); } + private _detachModel() { + this._localStore.clear(); + this._originalWebview?.dispose(); + this._originalWebview?.element.remove(); + this._originalWebview = null; + this._modifiedWebview?.dispose(); + this._modifiedWebview?.element.remove(); + this._modifiedWebview = null; + + this._modifiedResourceDisposableStore.clear(); + this._list.clear(); + + } + private _attachModel() { + this._eventDispatcher = new NotebookDiffEditorEventDispatcher(); + const updateInsets = () => { + DOM.scheduleAtNextAnimationFrame(() => { + if (this._isDisposed) { + return; + } + + if (this._modifiedWebview) { + this._updateOutputsOffsetsInWebview(this._list.scrollTop, this._list.scrollHeight, this._modifiedWebview, (diffElement: DiffElementViewModelBase) => { + return diffElement.modified; + }, DiffSide.Modified); + } + + if (this._originalWebview) { + this._updateOutputsOffsetsInWebview(this._list.scrollTop, this._list.scrollHeight, this._originalWebview, (diffElement: DiffElementViewModelBase) => { + return diffElement.original; + }, DiffSide.Original); + } + }); + }; + + this._localStore.add(this._list.onDidChangeContentHeight(() => { + updateInsets(); + })); + + this._localStore.add(this._eventDispatcher.onDidChangeCellLayout(() => { + updateInsets(); + })); + } + + private async _createModifiedWebview(id: string, resource: URI): Promise { + this._modifiedWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, { outputNodePadding: CELL_OUTPUT_PADDING, outputNodeLeftPadding: 32 }) as BackLayerWebView; + // attach the webview container to the DOM tree first + this._list.rowsContainer.insertAdjacentElement('afterbegin', this._modifiedWebview.element); + await this._modifiedWebview.createWebview(); + this._modifiedWebview.element.style.width = `calc(50% - 16px)`; + this._modifiedWebview.element.style.left = `calc(50%)`; + } + + private async _createOriginalWebview(id: string, resource: URI): Promise { + this._originalWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, { outputNodePadding: CELL_OUTPUT_PADDING, outputNodeLeftPadding: 32 }) as BackLayerWebView; + // attach the webview container to the DOM tree first + this._list.rowsContainer.insertAdjacentElement('afterbegin', this._originalWebview.element); + await this._originalWebview.createWebview(); + this._originalWebview.element.style.width = `calc(50% - 16px)`; + this._originalWebview.element.style.left = `16px`; + } + private async _resolveStats(resource: URI) { if (resource.scheme === Schemas.untitled) { return undefined; @@ -222,7 +415,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD const diffResult = await this.notebookEditorWorkerService.computeDiff(this._model.original.resource, this._model.modified.resource); const cellChanges = diffResult.cellsDiff.changes; - const cellDiffViewModels: CellDiffViewModel[] = []; + const diffElementViewModels: DiffElementViewModelBase[] = []; const originalModel = this._model.original.notebook; const modifiedModel = this._model.modified.notebook; let originalCellIndex = 0; @@ -238,20 +431,24 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD const originalCell = originalModel.cells[originalCellIndex + j]; const modifiedCell = modifiedModel.cells[modifiedCellIndex + j]; if (originalCell.getHashValue() === modifiedCell.getHashValue()) { - cellDiffViewModels.push(new CellDiffViewModel( - originalCell, - modifiedCell, + diffElementViewModels.push(new SideBySideDiffElementViewModel( + this._model.modified.notebook, + this._model.original.notebook, + this.instantiationService.createInstance(DiffNestedCellViewModel, originalCell), + this.instantiationService.createInstance(DiffNestedCellViewModel, modifiedCell), 'unchanged', this._eventDispatcher! )); } else { if (firstChangeIndex === -1) { - firstChangeIndex = cellDiffViewModels.length; + firstChangeIndex = diffElementViewModels.length; } - cellDiffViewModels.push(new CellDiffViewModel( - originalCell, - modifiedCell, + diffElementViewModels.push(new SideBySideDiffElementViewModel( + this._model.modified.notebook, + this._model.original.notebook, + this.instantiationService.createInstance(DiffNestedCellViewModel, originalCell), + this.instantiationService.createInstance(DiffNestedCellViewModel, modifiedCell), 'modified', this._eventDispatcher! )); @@ -260,24 +457,35 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD const modifiedLCS = this._computeModifiedLCS(change, originalModel, modifiedModel); if (modifiedLCS.length && firstChangeIndex === -1) { - firstChangeIndex = cellDiffViewModels.length; + firstChangeIndex = diffElementViewModels.length; } - cellDiffViewModels.push(...modifiedLCS); + diffElementViewModels.push(...modifiedLCS); originalCellIndex = change.originalStart + change.originalLength; modifiedCellIndex = change.modifiedStart + change.modifiedLength; } for (let i = originalCellIndex; i < originalModel.cells.length; i++) { - cellDiffViewModels.push(new CellDiffViewModel( - originalModel.cells[i], - modifiedModel.cells[i - originalCellIndex + modifiedCellIndex], + diffElementViewModels.push(new SideBySideDiffElementViewModel( + this._model.modified.notebook, + this._model.original.notebook, + this.instantiationService.createInstance(DiffNestedCellViewModel, originalModel.cells[i]), + this.instantiationService.createInstance(DiffNestedCellViewModel, modifiedModel.cells[i - originalCellIndex + modifiedCellIndex]), 'unchanged', this._eventDispatcher! )); } - this._list.splice(0, this._list.length, cellDiffViewModels); + this._originalWebview?.insetMapping.forEach((value, key) => { + this._originalWebview?.removeInset(key); + }); + + this._modifiedWebview?.insetMapping.forEach((value, key) => { + this._modifiedWebview?.removeInset(key); + }); + + this._diffElementViewModels = diffElementViewModels; + this._list.splice(0, this._list.length, diffElementViewModels); if (this._revealFirst && firstChangeIndex !== -1) { this._revealFirst = false; @@ -287,14 +495,16 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } private _computeModifiedLCS(change: IDiffChange, originalModel: NotebookTextModel, modifiedModel: NotebookTextModel) { - const result: CellDiffViewModel[] = []; + const result: DiffElementViewModelBase[] = []; // modified cells const modifiedLen = Math.min(change.originalLength, change.modifiedLength); for (let j = 0; j < modifiedLen; j++) { - result.push(new CellDiffViewModel( - originalModel.cells[change.originalStart + j], - modifiedModel.cells[change.modifiedStart + j], + result.push(new SideBySideDiffElementViewModel( + modifiedModel, + originalModel, + this.instantiationService.createInstance(DiffNestedCellViewModel, originalModel.cells[change.originalStart + j]), + this.instantiationService.createInstance(DiffNestedCellViewModel, modifiedModel.cells[change.modifiedStart + j]), 'modified', this._eventDispatcher! )); @@ -302,8 +512,10 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD for (let j = modifiedLen; j < change.originalLength; j++) { // deletion - result.push(new CellDiffViewModel( - originalModel.cells[change.originalStart + j], + result.push(new SingleSideDiffElementViewModel( + originalModel, + modifiedModel, + this.instantiationService.createInstance(DiffNestedCellViewModel, originalModel.cells[change.originalStart + j]), undefined, 'delete', this._eventDispatcher! @@ -312,9 +524,11 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD for (let j = modifiedLen; j < change.modifiedLength; j++) { // insertion - result.push(new CellDiffViewModel( + result.push(new SingleSideDiffElementViewModel( + modifiedModel, + originalModel, undefined, - modifiedModel.cells[change.modifiedStart + j], + this.instantiationService.createInstance(DiffNestedCellViewModel, modifiedModel.cells[change.modifiedStart + j]), 'insert', this._eventDispatcher! )); @@ -323,14 +537,12 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD return result; } - private pendingLayouts = new WeakMap(); + private pendingLayouts = new WeakMap(); - layoutNotebookCell(cell: CellDiffViewModel, height: number) { - const relayout = (cell: CellDiffViewModel, height: number) => { - const viewIndex = this._list.indexOf(cell); - - this._list?.updateElementHeight(viewIndex, height); + layoutNotebookCell(cell: DiffElementViewModelBase, height: number) { + const relayout = (cell: DiffElementViewModelBase, height: number) => { + this._list.updateElementHeight2(cell, height); }; if (this.pendingLayouts.has(cell)) { @@ -353,6 +565,79 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD return new Promise(resolve => { r = resolve; }); } + triggerScroll(event: IMouseWheelEvent) { + this._list.triggerScrollFromMouseWheelEvent(event); + } + + createInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: DiffNestedCellViewModel, output: IInsetRenderOutput, getOffset: () => number, diffSide: DiffSide): void { + this._insetModifyQueueByOutputId.queue(output.source.model.outputId + (diffSide === DiffSide.Modified ? '-right' : 'left'), async () => { + const activeWebview = diffSide === DiffSide.Modified ? this._modifiedWebview : this._originalWebview; + if (!activeWebview) { + return; + } + + if (!activeWebview.insetMapping.has(output.source)) { + const cellTop = this._list.getAbsoluteTopOfElement(cellDiffViewModel); + await activeWebview.createInset({ diffElement: cellDiffViewModel, cellHandle: cellViewModel.handle, cellId: cellViewModel.id, cellUri: cellViewModel.uri }, output, cellTop, getOffset()); + } else { + const cellTop = this._list.getAbsoluteTopOfElement(cellDiffViewModel); + const scrollTop = this._list.scrollTop; + const outputIndex = cellViewModel.outputsViewModels.indexOf(output.source); + const outputOffset = cellTop + cellDiffViewModel.getOutputOffsetInCell(diffSide, outputIndex); + activeWebview.updateViewScrollTop(-scrollTop, true, [{ output: output.source, cellTop, outputOffset }]); + } + }); + } + + getCellByInfo(cellInfo: IDiffCellInfo): IGenericCellViewModel { + return cellInfo.diffElement.getCellByUri(cellInfo.cellUri); + } + + removeInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: DiffNestedCellViewModel, displayOutput: IDisplayOutputViewModel, diffSide: DiffSide) { + this._insetModifyQueueByOutputId.queue(displayOutput.model.outputId + (diffSide === DiffSide.Modified ? '-right' : 'left'), async () => { + const activeWebview = diffSide === DiffSide.Modified ? this._modifiedWebview : this._originalWebview; + if (!activeWebview) { + return; + } + + if (!activeWebview.insetMapping.has(displayOutput)) { + return; + } + + activeWebview.removeInset(displayOutput); + }); + } + + showInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: DiffNestedCellViewModel, displayOutput: IDisplayOutputViewModel, diffSide: DiffSide) { + this._insetModifyQueueByOutputId.queue(displayOutput.model.outputId + (diffSide === DiffSide.Modified ? '-right' : 'left'), async () => { + const activeWebview = diffSide === DiffSide.Modified ? this._modifiedWebview : this._originalWebview; + if (!activeWebview) { + return; + } + + if (!activeWebview.insetMapping.has(displayOutput)) { + return; + } + + const cellTop = this._list.getAbsoluteTopOfElement(cellDiffViewModel); + const scrollTop = this._list.scrollTop; + const outputIndex = cellViewModel.outputsViewModels.indexOf(displayOutput); + const outputOffset = cellTop + cellDiffViewModel.getOutputOffsetInCell(diffSide, outputIndex); + activeWebview.updateViewScrollTop(-scrollTop, true, [{ output: displayOutput, cellTop, outputOffset }]); + }); + } + + hideInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: DiffNestedCellViewModel, output: IDisplayOutputViewModel) { + this._modifiedWebview?.hideInset(output); + this._originalWebview?.hideInset(output); + } + + // private async _resolveWebview(rightEditor: boolean): Promise { + // if (rightEditor) { + + // } + // } + getDomNode() { return this._rootElement; } @@ -380,6 +665,18 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._list?.splice(0, this._list?.length || 0); } + getOutputRenderer(): OutputRenderer { + return this._outputRenderer; + } + + deltaCellOutputContainerClassNames(diffSide: DiffSide, cellId: string, added: string[], removed: string[]) { + if (diffSide === DiffSide.Original) { + this._originalWebview?.deltaCellOutputContainerClassNames(cellId, added, removed); + } else { + this._modifiedWebview?.deltaCellOutputContainerClassNames(cellId, added, removed); + } + } + getLayoutInfo(): NotebookLayoutInfo { if (!this._list) { throw new Error('Editor is not initalized successfully'); @@ -392,6 +689,56 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD }; } + getCellOutputLayoutInfo(nestedCell: DiffNestedCellViewModel) { + if (!this._model) { + throw new Error('Editor is not attached to model yet'); + } + const documentModel = CellUri.parse(nestedCell.uri); + if (!documentModel) { + throw new Error('Nested cell in the diff editor has wrong Uri'); + } + + const belongToOriginalDocument = this._model.original.notebook.uri.toString() === documentModel.notebook.toString(); + const viewModel = this._diffElementViewModels.find(element => { + const textModel = belongToOriginalDocument ? element.original : element.modified; + if (!textModel) { + return false; + } + + if (textModel.uri.toString() === nestedCell.uri.toString()) { + return true; + } + + return false; + }); + + if (!viewModel) { + throw new Error('Nested cell in the diff editor does not match any diff element'); + } + + if (viewModel.type === 'unchanged') { + return this.getLayoutInfo(); + } + + if (viewModel.type === 'insert' || viewModel.type === 'delete') { + return { + width: this._dimension!.width / 2, + height: this._dimension!.height / 2, + fontInfo: this._fontInfo! + }; + } + + if (viewModel.checkIfOutputsModified()) { + return { + width: this._dimension!.width / 2, + height: this._dimension!.height / 2, + fontInfo: this._fontInfo! + }; + } else { + return this.getLayoutInfo(); + } + } + layout(dimension: DOM.Dimension): void { this._rootElement.classList.toggle('mid-width', dimension.width < 1000 && dimension.width >= 600); this._rootElement.classList.toggle('narrow-width', dimension.width < 600); @@ -399,14 +746,39 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._rootElement.style.height = `${dimension.height}px`; this._list?.layout(this._dimension.height, this._dimension.width); - this._eventDispatcher?.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + + + if (this._modifiedWebview) { + this._modifiedWebview.element.style.width = `calc(50% - 16px)`; + this._modifiedWebview.element.style.left = `calc(50%)`; + } + + if (this._originalWebview) { + this._originalWebview.element.style.width = `calc(50% - 16px)`; + this._originalWebview.element.style.left = `16px`; + } + + if (this._webviewTransparentCover) { + this._webviewTransparentCover.style.height = `${dimension.height}px`; + this._webviewTransparentCover.style.width = `${dimension.width}px`; + } + + this._eventDispatcher?.emit([new NotebookDiffLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + } + + dispose() { + this._isDisposed = true; + super.dispose(); } } registerThemingParticipant((theme, collector) => { const cellBorderColor = theme.getColor(notebookCellBorder); if (cellBorderColor) { - collector.addRule(`.notebook-text-diff-editor .cell-body { border: 1px solid ${cellBorderColor};}`); + collector.addRule(`.notebook-text-diff-editor .cell-body .border-container .top-border { border-top: 1px solid ${cellBorderColor};}`); + collector.addRule(`.notebook-text-diff-editor .cell-body .border-container .bottom-border { border-top: 1px solid ${cellBorderColor};}`); + collector.addRule(`.notebook-text-diff-editor .cell-body .border-container .left-border { border-left: 1px solid ${cellBorderColor};}`); + collector.addRule(`.notebook-text-diff-editor .cell-body .border-container .right-border { border-right: 1px solid ${cellBorderColor};}`); collector.addRule(`.notebook-text-diff-editor .cell-diff-editor-container .output-header-container, .notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container { border-top: 1px solid ${cellBorderColor}; @@ -429,6 +801,13 @@ registerThemingParticipant((theme, collector) => { const added = theme.getColor(diffInserted); if (added) { + collector.addRule( + ` + .monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-right div.foreground { background-color: ${added}; } + .monaco-workbench .notebook-text-diff-editor .cell-body.right .output-info-container .output-view-container div.foreground { background-color: ${added}; } + .monaco-workbench .notebook-text-diff-editor .cell-body.right .output-info-container .output-view-container div.output-empty-view { background-color: ${added}; } + ` + ); collector.addRule(` .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .source-container { background-color: ${added}; } .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .source-container .monaco-editor .margin, @@ -460,7 +839,15 @@ registerThemingParticipant((theme, collector) => { ); } const removed = theme.getColor(diffRemoved); - if (added) { + if (removed) { + collector.addRule( + ` + .monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-left div.foreground { background-color: ${removed}; } + .monaco-workbench .notebook-text-diff-editor .cell-body.left .output-info-container .output-view-container div.foreground { background-color: ${removed}; } + .monaco-workbench .notebook-text-diff-editor .cell-body.left .output-info-container .output-view-container div.output-empty-view { background-color: ${removed}; } + + ` + ); collector.addRule(` .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .source-container { background-color: ${removed}; } .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .source-container .monaco-editor .margin, diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts index 6e664d46..ce2bc9c6 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts @@ -14,12 +14,20 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IListService, IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/listService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; -import { CellDiffRenderTemplate, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/common'; +import { DiffElementViewModelBase, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { CellDiffSideBySideRenderTemplate, CellDiffSingleSideRenderTemplate, DIFF_CELL_MARGIN, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; import { isMacintosh } from 'vs/base/common/platform'; -import { DeletedCell, InsertCell, ModifiedCell } from 'vs/workbench/contrib/notebook/browser/diff/cellComponents'; +import { DeletedElement, fixedDiffEditorOptions, fixedEditorOptions, getOptimizedNestedCodeEditorWidgetOptions, InsertElement, ModifiedElement } from 'vs/workbench/contrib/notebook/browser/diff/diffComponents'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellActionView'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; -export class NotebookCellTextDiffListDelegate implements IListVirtualDelegate { +export class NotebookCellTextDiffListDelegate implements IListVirtualDelegate { // private readonly lineHeight: number; constructor( @@ -29,20 +37,28 @@ export class NotebookCellTextDiffListDelegate implements IListVirtualDelegate { - static readonly TEMPLATE_ID = 'cell_diff'; +export class CellDiffSingleSideRenderer implements IListRenderer { + static readonly TEMPLATE_ID = 'cell_diff_single'; constructor( readonly notebookEditor: INotebookTextDiffEditor, @@ -50,56 +66,228 @@ export class CellDiffRenderer implements IListRenderer implements IDisposable, IStyleController { +export class CellDiffSideBySideRenderer implements IListRenderer { + static readonly TEMPLATE_ID = 'cell_diff_side_by_side'; + + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IContextMenuService protected readonly contextMenuService: IContextMenuService, + @IKeybindingService protected readonly keybindingService: IKeybindingService, + @IMenuService protected readonly menuService: IMenuService, + @IContextKeyService protected readonly contextKeyService: IContextKeyService, + @INotificationService protected readonly notificationService: INotificationService, + ) { } + + get templateId() { + return CellDiffSideBySideRenderer.TEMPLATE_ID; + } + + renderTemplate(container: HTMLElement): CellDiffSideBySideRenderTemplate { + const body = DOM.$('.cell-body'); + DOM.append(container, body); + const diffEditorContainer = DOM.$('.cell-diff-editor-container'); + DOM.append(body, diffEditorContainer); + + const sourceContainer = DOM.append(diffEditorContainer, DOM.$('.source-container')); + const { editor, editorContainer } = this._buildSourceEditor(sourceContainer); + + const inputToolbarContainer = DOM.append(sourceContainer, DOM.$('.editor-input-toolbar-container')); + const cellToolbarContainer = DOM.append(inputToolbarContainer, DOM.$('div.property-toolbar')); + const toolbar = new ToolBar(cellToolbarContainer, this.contextMenuService, { + actionViewItemProvider: action => { + if (action instanceof MenuItemAction) { + const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService); + return item; + } + + return undefined; + } + }); + + const metadataHeaderContainer = DOM.append(diffEditorContainer, DOM.$('.metadata-header-container')); + const metadataInfoContainer = DOM.append(diffEditorContainer, DOM.$('.metadata-info-container')); + + const outputHeaderContainer = DOM.append(diffEditorContainer, DOM.$('.output-header-container')); + const outputInfoContainer = DOM.append(diffEditorContainer, DOM.$('.output-info-container')); + + const borderContainer = DOM.append(body, DOM.$('.border-container')); + const leftBorder = DOM.append(borderContainer, DOM.$('.left-border')); + const rightBorder = DOM.append(borderContainer, DOM.$('.right-border')); + const topBorder = DOM.append(borderContainer, DOM.$('.top-border')); + const bottomBorder = DOM.append(borderContainer, DOM.$('.bottom-border')); + + + return { + body, + container, + diffEditorContainer, + sourceEditor: editor, + editorContainer, + inputToolbarContainer, + toolbar, + metadataHeaderContainer, + metadataInfoContainer, + outputHeaderContainer, + outputInfoContainer, + leftBorder, + rightBorder, + topBorder, + bottomBorder, + elementDisposables: new DisposableStore() + }; + } + + private _buildSourceEditor(sourceContainer: HTMLElement) { + const editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container')); + + const editor = this.instantiationService.createInstance(DiffEditorWidget, editorContainer, { + ...fixedDiffEditorOptions, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + originalEditable: false, + ignoreTrimWhitespace: false, + automaticLayout: false, + dimension: { + height: 0, + width: 0 + } + }, { + originalEditor: getOptimizedNestedCodeEditorWidgetOptions(), + modifiedEditor: getOptimizedNestedCodeEditorWidgetOptions() + }); + + return { + editor, + editorContainer + }; + } + + renderElement(element: SideBySideDiffElementViewModel, index: number, templateData: CellDiffSideBySideRenderTemplate, height: number | undefined): void { + templateData.body.classList.remove('left', 'right', 'full'); + + switch (element.type) { + case 'unchanged': + templateData.elementDisposables.add(this.instantiationService.createInstance(ModifiedElement, this.notebookEditor, element, templateData)); + return; + case 'modified': + templateData.elementDisposables.add(this.instantiationService.createInstance(ModifiedElement, this.notebookEditor, element, templateData)); + return; + default: + break; + } + } + + disposeTemplate(templateData: CellDiffSideBySideRenderTemplate): void { + templateData.container.innerText = ''; + templateData.sourceEditor.dispose(); + templateData.toolbar?.dispose(); + } + + disposeElement(element: SideBySideDiffElementViewModel, index: number, templateData: CellDiffSideBySideRenderTemplate): void { + templateData.elementDisposables.clear(); + } +} + +export class NotebookTextDiffList extends WorkbenchList implements IDisposable, IStyleController { private styleElement?: HTMLStyleElement; + get rowsContainer(): HTMLElement { + return this.view.containerDomNode; + } + constructor( listUser: string, container: HTMLElement, - delegate: IListVirtualDelegate, - renderers: IListRenderer[], + delegate: IListVirtualDelegate, + renderers: IListRenderer[], contextKeyService: IContextKeyService, - options: IWorkbenchListOptions, + options: IWorkbenchListOptions, @IListService listService: IListService, @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, @@ -107,6 +295,32 @@ export class NotebookTextDiffList extends WorkbenchList imple super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService); } + getAbsoluteTopOfElement(element: DiffElementViewModelBase): number { + const index = this.indexOf(element); + // if (index === undefined || index < 0 || index >= this.length) { + // this._getViewIndexUpperBound(element); + // throw new ListError(this.listUser, `Invalid index ${index}`); + // } + + return this.view.elementTop(index); + } + + triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) { + this.view.triggerScrollFromMouseWheelEvent(browserEvent); + } + + clear() { + super.splice(0, this.length); + } + + + updateElementHeight2(element: DiffElementViewModelBase, size: number) { + const viewIndex = this.indexOf(element); + const focused = this.getFocus(); + + this.view.updateElementHeight(viewIndex, size, focused.length ? focused[0] : null); + } + style(styles: IListStyles) { const selectorSuffix = this.view.domId; if (!this.styleElement) { diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index c2b2830c..09cb20b0 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -184,11 +184,15 @@ .monaco-workbench .notebookOverlay .output .multi-mimetype-output { position: absolute; top: 4px; - left: -26px; + left: -30px; width: 16px; height: 16px; cursor: pointer; - padding: 4px; + padding: 6px; +} + +.monaco-workbench .notebookOverlay .output pre { + margin: 4px 0; } .monaco-workbench .notebookOverlay .output .error_message { @@ -248,15 +252,17 @@ } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-collapsed-part { + position: relative; box-sizing: border-box; } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-collapsed-part .codicon { - margin-top: 4px; - position: relative; - left: -23px; + position: absolute; + padding: 2px 6px; + left: -30px; + bottom: 0; cursor: pointer; - z-index: 27; /* Over drag handle */ + z-index: 29; /* Over drag handle and bottom cell toolbar */ } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.collapsed .notebook-folding-indicator, @@ -475,6 +481,10 @@ text-align: center; } +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .output-collapsed .execution-count-label { + bottom: 24px; +} + .monaco-workbench .notebookOverlay .cell .cell-editor-part { position: relative; } @@ -585,7 +595,7 @@ display: flex; align-items: center; justify-content: center; - z-index: 25; /* over the focus outline on the editor, below the title toolbar */ + z-index: 28; /* over the focus outline on the editor, below the title toolbar */ width: 100%; opacity: 0; transition: opacity 0.2s ease-in-out; @@ -598,8 +608,9 @@ .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container:focus-within, .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container:hover, -.monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:focus-within, -.monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:hover { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell-bottom-toolbar-container, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list:focus-within > .monaco-scrollable-element > .monaco-list-rows:not(:hover) > .monaco-list-row.focused .cell-bottom-toolbar-container, +.monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:focus-within { opacity: 1; } @@ -863,3 +874,18 @@ .cell-contributed-items.cell-contributed-items-left { margin-left: 4px; } + +.monaco-workbench .notebookOverlay .output-plaintext::-webkit-scrollbar { + width: 10px; /* width of the entire scrollbar */ + height: 10px; +} + +.monaco-workbench .notebookOverlay .output-plaintext::-webkit-scrollbar-thumb { + height: 10px; + width: 10px; +} + +.monaco-workbench .notebookOverlay .output .output-plaintext { + margin: 4px 0; + overflow-x: auto; +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index b3e9c30f..9289de4d 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -31,12 +31,12 @@ import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/noteb import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookServiceImpl'; import { CellKind, CellToolbarLocKey, CellUri, DisplayOrderKey, getCellUndoRedoComparisonKey, NotebookDocumentBackupData, NotebookEditorPriority, NotebookTextDiffEditorPreview, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -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 { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { CustomEditorsAssociations, customEditorsAssociationsSettingId } from 'vs/workbench/services/editor/common/editorOpenWith'; import { CustomEditorInfo } from 'vs/workbench/contrib/customEditor/common/customEditor'; -import { INotebookEditor, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditor, IN_NOTEBOOK_TEXT_DIFF_EDITOR, NotebookEditorOptions, NOTEBOOK_EDITOR_OPEN } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { INotebookEditorModelResolverService, NotebookModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; @@ -47,11 +47,15 @@ import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/comm import { NotebookEditorWorkerServiceImpl } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerServiceImpl'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; import { NotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl'; +import { INotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetService'; +import { NotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetServiceImpl'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { Event } from 'vs/base/common/event'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { getFormatedMetadataJSON } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; // Editor Contribution @@ -59,7 +63,7 @@ import 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; import 'vs/workbench/contrib/notebook/browser/contrib/find/findController'; import 'vs/workbench/contrib/notebook/browser/contrib/fold/folding'; import 'vs/workbench/contrib/notebook/browser/contrib/format/formatting'; -import 'vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider'; +import 'vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline'; import 'vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider'; import 'vs/workbench/contrib/notebook/browser/contrib/status/editorStatus'; // import 'vs/workbench/contrib/notebook/browser/contrib/scm/scm'; @@ -204,17 +208,24 @@ Registry.as(EditorInputExtensions.EditorInputFactor ); export class NotebookContribution extends Disposable implements IWorkbenchContribution { + private _notebookEditorIsOpen: IContextKey; + private _notebookDiffEditorIsOpen: IContextKey; constructor( + @IContextKeyService private readonly contextKeyService: IContextKeyService, @IEditorService private readonly editorService: IEditorService, @INotebookService private readonly notebookService: INotebookService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IConfigurationService private readonly configurationService: IConfigurationService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, + @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IUndoRedoService undoRedoService: IUndoRedoService, ) { super(); + this._notebookEditorIsOpen = NOTEBOOK_EDITOR_OPEN.bindTo(this.contextKeyService); + this._notebookDiffEditorIsOpen = IN_NOTEBOOK_TEXT_DIFF_EDITOR.bindTo(this.contextKeyService); + this._register(undoRedoService.registerUriComparisonKeyComputer(CellUri.scheme, { getComparisonKey: (uri: URI): string => { return getCellUndoRedoComparisonKey(uri); @@ -224,12 +235,28 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri this._register(this.editorService.overrideOpenEditor({ getEditorOverrides: (resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined) => { - const currentEditorForResource = group?.editors.find(editor => isEqual(editor.resource, resource)); + const currentEditorsForResource = group && this.editorService.findEditors(resource, group); + const currentEditorForResource = currentEditorsForResource && currentEditorsForResource.length ? currentEditorsForResource[0] : undefined; const associatedEditors = distinct([ ...this.getUserAssociatedNotebookEditors(resource), ...this.getContributedEditors(resource) - ], editor => editor.id); + ], editor => editor.id).sort((a, b) => { + // if a content provider is exclusive, it has higher order + if (a.exclusive && b.exclusive) { + return 0; + } + + if (a.exclusive) { + return -1; + } + + if (b.exclusive) { + return 1; + } + + return 0; + }); return associatedEditors.map(info => { return { @@ -264,6 +291,19 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri this.notebookService.updateActiveNotebookEditor(null); } })); + + this.editorGroupService.whenRestored.then(() => this._updateContextKeys()); + this._register(this.editorService.onDidActiveEditorChange(() => this._updateContextKeys())); + this._register(this.editorService.onDidVisibleEditorsChange(() => this._updateContextKeys())); + + this._register(this.editorGroupService.onDidAddGroup(() => this._updateContextKeys())); + this._register(this.editorGroupService.onDidRemoveGroup(() => this._updateContextKeys())); + } + + private _updateContextKeys() { + const activeEditorPane = this.editorService.activeEditorPane as { isNotebookEditor?: boolean } | undefined; + this._notebookEditorIsOpen.set(!!activeEditorPane?.isNotebookEditor); + this._notebookDiffEditorIsOpen.set(this.editorService.activeEditorPane?.getId() === 'workbench.editor.notebookTextDiffEditor'); } getUserAssociatedEditors(resource: URI) { @@ -300,8 +340,70 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri return undefined; } - if (originalInput instanceof NotebookEditorInput) { - return undefined; + // Run reopen with ... + if (id) { + // from the editor tab context menu + if (originalInput instanceof NotebookEditorInput) { + if (originalInput.viewType === id) { + // reopen with the same type + return undefined; + } else { + return { + override: (async () => { + const notebookInput = NotebookEditorInput.create(this.instantiationService, originalInput.resource, originalInput.getName(), id); + const originalEditorIndex = group.getIndexOfEditor(originalInput); + + await group.closeEditor(originalInput); + originalInput.dispose(); + const newEditor = await group.openEditor(notebookInput, { ...options, index: originalEditorIndex, override: false }); + if (newEditor) { + return newEditor; + } else { + return undefined; + } + })() + }; + } + } else { + // from the file explorer + const existingEditors = this.editorService.findEditors(originalInput.resource, group).filter(editor => editor instanceof NotebookEditorInput) as NotebookEditorInput[]; + + if (existingEditors.length) { + // there are notebook editors with the same resource + + if (existingEditors.find(editor => editor.viewType === id)) { + return { override: this.editorService.openEditor(existingEditors.find(editor => editor.viewType === id)!, { ...options, override: false }, group) }; + } else { + return { + override: (async () => { + const firstEditor = existingEditors[0]!; + const originalEditorIndex = group.getIndexOfEditor(firstEditor); + + await group.closeEditor(firstEditor); + firstEditor.dispose(); + const notebookInput = NotebookEditorInput.create(this.instantiationService, originalInput.resource!, originalInput.getName(), id); + const newEditor = await group.openEditor(notebookInput, { ...options, index: originalEditorIndex, override: false }); + + if (newEditor) { + return newEditor; + } else { + return undefined; + } + })() + }; + } + } + } + } + + // Click on the editor tab + if (id === undefined && originalInput instanceof NotebookEditorInput) { + const existingEditors = this.editorService.findEditors(originalInput.resource, group).filter(editor => editor instanceof NotebookEditorInput && editor === originalInput); + + if (existingEditors.length) { + // same + return undefined; + } } if (originalInput instanceof NotebookDiffEditorInput) { @@ -323,10 +425,8 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri } if (id === undefined) { - const existingEditors = group.editors.filter(editor => - editor.resource - && isEqual(editor.resource, notebookUri) - && !(editor instanceof NotebookEditorInput) + const existingEditors = this.editorService.findEditors(notebookUri, group).filter(editor => + !(editor instanceof NotebookEditorInput) && !(editor instanceof NotebookDiffEditorInput) ); @@ -391,7 +491,7 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri return undefined; } - const existingEditors = group.editors.filter(editor => editor.resource && isEqual(editor.resource, notebookUri) && !(editor instanceof NotebookEditorInput)); + const existingEditors = this.editorService.findEditors(notebookUri, group).filter(editor => !(editor instanceof NotebookEditorInput)); if (existingEditors.length) { return undefined; @@ -462,7 +562,7 @@ class CellContentProvider implements ITextModelContentProvider { create: (defaultEOL) => { const newEOL = (defaultEOL === DefaultEndOfLine.CRLF ? '\r\n' : '\n'); (cell.textBuffer as ITextBuffer).setEOL(newEOL); - return cell.textBuffer as ITextBuffer; + return { textBuffer: cell.textBuffer as ITextBuffer, disposable: Disposable.None }; }, getFirstLineText: (limit: number) => { return cell.textBuffer.getLineContent(1).substr(0, limit); @@ -489,6 +589,61 @@ class CellContentProvider implements ITextModelContentProvider { } } +class CellMetadataContentProvider implements ITextModelContentProvider { + private readonly _registration: IDisposable; + + constructor( + @ITextModelService textModelService: ITextModelService, + @IModelService private readonly _modelService: IModelService, + @IModeService private readonly _modeService: IModeService, + @INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService, + ) { + this._registration = textModelService.registerTextModelContentProvider(Schemas.vscodeNotebookCellMetadata, this); + } + + dispose(): void { + this._registration.dispose(); + } + + async provideTextContent(resource: URI): Promise { + const existing = this._modelService.getModel(resource); + if (existing) { + return existing; + } + const data = CellUri.parseCellMetadataUri(resource); + // const data = parseCellUri(resource); + if (!data) { + return null; + } + + const ref = await this._notebookModelResolverService.resolve(data.notebook); + let result: ITextModel | null = null; + + const mode = this._modeService.create('json'); + + for (const cell of ref.object.notebook.cells) { + if (cell.handle === data.handle) { + const metadataSource = getFormatedMetadataJSON(ref.object.notebook, cell.metadata || {}, cell.language); + result = this._modelService.createModel( + metadataSource, + mode, + resource + ); + break; + } + } + + if (result) { + const once = result.onWillDispose(() => { + once.dispose(); + ref.dispose(); + }); + } + + return result; + } +} + class RegisterSchemasContribution extends Disposable implements IWorkbenchContribution { constructor() { super(); @@ -596,6 +751,7 @@ class NotebookFileTracker implements IWorkbenchContribution { const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(NotebookContribution, LifecyclePhase.Starting); workbenchContributionsRegistry.registerWorkbenchContribution(CellContentProvider, LifecyclePhase.Starting); +workbenchContributionsRegistry.registerWorkbenchContribution(CellMetadataContentProvider, LifecyclePhase.Starting); workbenchContributionsRegistry.registerWorkbenchContribution(RegisterSchemasContribution, LifecyclePhase.Starting); workbenchContributionsRegistry.registerWorkbenchContribution(NotebookFileTracker, LifecyclePhase.Ready); @@ -603,6 +759,7 @@ registerSingleton(INotebookService, NotebookService); registerSingleton(INotebookEditorWorkerService, NotebookEditorWorkerServiceImpl); registerSingleton(INotebookEditorModelResolverService, NotebookModelResolverService, true); registerSingleton(INotebookCellStatusBarService, NotebookCellStatusBarService, true); +registerSingleton(INotebookEditorWidgetService, NotebookEditorWidgetService, true); const configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration({ diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 1633a9f7..9f624090 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -22,7 +22,7 @@ import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/outpu import { RunStateRenderer, TimerRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, IProcessedOutput, IRenderOutput, NotebookCellMetadata, NotebookDocumentMetadata, IEditor, INotebookKernelInfo2, IInsetRenderOutput, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, IProcessedOutput, NotebookCellMetadata, NotebookDocumentMetadata, IEditor, INotebookKernelInfo2, ICellRange, IOrderedMimeType, ITransformedDisplayOutputDto, INotebookRendererInfo, IErrorOutput, IStreamOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { IMenu } from 'vs/platform/actions/common/actions'; @@ -32,6 +32,7 @@ import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation'; import { CellEditorStatusBar } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets'; +//#region Context Keys export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey('notebookFindWidgetFocused', false); // Is Notebook @@ -39,12 +40,16 @@ export const NOTEBOOK_IS_ACTIVE_EDITOR = ContextKeyExpr.equals('activeEditor', ' // Editor keys export const NOTEBOOK_EDITOR_FOCUSED = new RawContextKey('notebookEditorFocused', false); +export const NOTEBOOK_EDITOR_OPEN = new RawContextKey('notebookEditorOpen', false); export const NOTEBOOK_CELL_LIST_FOCUSED = new RawContextKey('notebookCellListFocused', false); export const NOTEBOOK_OUTPUT_FOCUSED = new RawContextKey('notebookOutputFocused', false); export const NOTEBOOK_EDITOR_EDITABLE = new RawContextKey('notebookEditable', true); export const NOTEBOOK_EDITOR_RUNNABLE = new RawContextKey('notebookRunnable', true); export const NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK = new RawContextKey('notebookExecuting', false); +// Diff Editor Keys +export const IN_NOTEBOOK_TEXT_DIFF_EDITOR = new RawContextKey('isInNotebookTextDiffEditor', false); + // Cell keys export const NOTEBOOK_VIEW_TYPE = new RawContextKey('notebookViewType', undefined); export const NOTEBOOK_CELL_TYPE = new RawContextKey('notebookCellType', undefined); // code, markdown @@ -57,14 +62,114 @@ export const NOTEBOOK_CELL_RUN_STATE = new RawContextKey('notebookCellRu export const NOTEBOOK_CELL_HAS_OUTPUTS = new RawContextKey('notebookCellHasOutputs', false); // bool export const NOTEBOOK_CELL_INPUT_COLLAPSED = new RawContextKey('notebookCellInputIsCollapsed', false); // bool export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = new RawContextKey('notebookCellOutputIsCollapsed', false); // bool +// Kernels +export const NOTEBOOK_HAS_MULTIPLE_KERNELS = new RawContextKey('notebookHasMultipleKernels', false); -// Shared commands +//#endregion + +//#region Shared commands export const EXPAND_CELL_CONTENT_COMMAND_ID = 'notebook.cell.expandCellContent'; export const EXECUTE_CELL_COMMAND_ID = 'notebook.cell.execute'; -// Kernels +//#endregion -export const NOTEBOOK_HAS_MULTIPLE_KERNELS = new RawContextKey('notebookHasMultipleKernels', false); +//#region Output related types +export const enum RenderOutputType { + None, + Html, + Extension +} + +export interface IRenderNoOutput { + type: RenderOutputType.None; + hasDynamicHeight: boolean; +} + +export interface IRenderPlainHtmlOutput { + type: RenderOutputType.Html; + source: IDisplayOutputViewModel; + htmlContent: string; + hasDynamicHeight: boolean; +} + +export interface IRenderOutputViaExtension { + type: RenderOutputType.Extension; + source: IDisplayOutputViewModel; + mimeType: string; + renderer: INotebookRendererInfo; +} + +export type IInsetRenderOutput = IRenderPlainHtmlOutput | IRenderOutputViaExtension; +export type IRenderOutput = IRenderNoOutput | IInsetRenderOutput; + +export const outputHasDynamicHeight = (o: IRenderOutput) => o.type !== RenderOutputType.Extension && o.hasDynamicHeight; + + +export interface ICellOutputViewModel { + cellViewModel: IGenericCellViewModel; + model: IProcessedOutput; + isDisplayOutput(): this is IDisplayOutputViewModel; + isErrorOutput(): this is IErrorOutputViewModel; + isStreamOutput(): this is IStreamOutputViewModel; +} + +export interface IDisplayOutputViewModel extends ICellOutputViewModel { + model: ITransformedDisplayOutputDto; + resolveMimeTypes(textModel: NotebookTextModel): [readonly IOrderedMimeType[], number]; + pickedMimeType: number; +} + +export interface IErrorOutputViewModel extends ICellOutputViewModel { + model: IErrorOutput; +} + +export interface IStreamOutputViewModel extends ICellOutputViewModel { + model: IStreamOutput; +} + +//#endregion + +//#region Shared types between the Notebook Editor and Notebook Diff Editor, they are mostly used for output rendering + +export interface IGenericCellViewModel { + id: string; + handle: number; + uri: URI; + metadata: NotebookCellMetadata | undefined; + outputIsHovered: boolean; + outputsViewModels: ICellOutputViewModel[]; + getOutputOffset(index: number): number; + updateOutputHeight(index: number, height: number): void; +} + +export interface IDisplayOutputLayoutUpdateRequest { + output: IDisplayOutputViewModel; + cellTop: number; + outputOffset: number; +} + +export interface ICommonCellInfo { + cellId: string; + cellHandle: number; + cellUri: URI; +} + +export interface INotebookCellOutputLayoutInfo { + width: number; + height: number; + fontInfo: BareFontInfo; +} + +export interface ICommonNotebookEditor { + getCellOutputLayoutInfo(cell: IGenericCellViewModel): INotebookCellOutputLayoutInfo; + triggerScroll(event: IMouseWheelEvent): void; + getCellByInfo(cellInfo: ICommonCellInfo): IGenericCellViewModel; + focusNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): void; + focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): void; + updateOutputHeight(cellInfo: ICommonCellInfo, output: IDisplayOutputViewModel, height: number, isInit: boolean): void; +} + +//#endregion export interface NotebookLayoutInfo { width: number; @@ -122,7 +227,7 @@ export interface MarkdownCellLayoutChangeEvent { totalHeight?: number; } -export interface ICellViewModel { +export interface ICellViewModel extends IGenericCellViewModel { readonly model: NotebookCellTextModel; readonly id: string; readonly textBuffer: IReadonlyTextBuffer; @@ -133,6 +238,7 @@ export interface ICellViewModel { cellKind: CellKind; editState: CellEditState; focusMode: CellFocusMode; + outputIsHovered: boolean; getText(): string; getTextLength(): number; metadata: NotebookCellMetadata | undefined; @@ -212,7 +318,7 @@ export interface IActiveNotebookEditor extends INotebookEditor { uri: URI; } -export interface INotebookEditor extends IEditor { +export interface INotebookEditor extends IEditor, ICommonNotebookEditor { isEmbedded: boolean; cursorNavigationMode: boolean; @@ -323,6 +429,8 @@ export interface INotebookEditor extends IEditor { */ focusNotebookCell(cell: ICellViewModel, focus: 'editor' | 'container' | 'output'): void; + focusNextNotebookCell(cell: ICellViewModel, focus: 'editor' | 'container' | 'output'): void; + /** * Execute the given notebook cell */ @@ -361,12 +469,12 @@ export interface INotebookEditor extends IEditor { /** * Remove the output from the webview layer */ - removeInset(output: IProcessedOutput): void; + removeInset(output: IDisplayOutputViewModel): void; /** * Hide the inset in the webview layer without removing it */ - hideInset(output: IProcessedOutput): void; + hideInset(output: IDisplayOutputViewModel): void; /** * Send message to the webview for outputs. @@ -395,11 +503,21 @@ export interface INotebookEditor extends IEditor { */ triggerScroll(event: IMouseWheelEvent): void; + /** + * The range will be revealed with as little scrolling as possible. + */ + revealCellRangeInView(range: ICellRange): void; + /** * Reveal cell into viewport. */ revealInView(cell: ICellViewModel): void; + /** + * Reveal cell into the top of viewport. + */ + revealInViewAtTop(cell: ICellViewModel): void; + /** * Reveal cell into viewport center. */ @@ -476,6 +594,9 @@ export interface INotebookEditor extends IEditor { * @return The contribution or null if contribution not found. */ getContribution(id: string): T; + + getCellByInfo(cellInfo: ICommonCellInfo): ICellViewModel; + updateOutputHeight(cellInfo: ICommonCellInfo, output: IDisplayOutputViewModel, height: number, isInit: boolean): void; } export interface INotebookCellList { @@ -494,8 +615,8 @@ export interface INotebookCellList { scrollLeft: number; length: number; rowsContainer: HTMLElement; - readonly onDidRemoveOutput: Event; - readonly onDidHideOutput: Event; + readonly onDidRemoveOutput: Event; + readonly onDidHideOutput: Event; readonly onMouseUp: Event>; readonly onMouseDown: Event>; readonly onContextMenu: Event>; @@ -506,7 +627,9 @@ export interface INotebookCellList { focusElement(element: ICellViewModel): void; selectElement(element: ICellViewModel): void; getFocusedElements(): ICellViewModel[]; + revealElementsInView(range: ICellRange): void; revealElementInView(element: ICellViewModel): void; + revealElementInViewAtTop(element: ICellViewModel): void; revealElementInCenterIfOutsideViewport(element: ICellViewModel): void; revealElementInCenter(element: ICellViewModel): void; revealElementInCenterIfOutsideViewportAsync(element: ICellViewModel): Promise; @@ -594,7 +717,7 @@ export interface IOutputTransformContribution { * This call is allowed to have side effects, such as placing output * directly into the container element. */ - render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI | undefined): IRenderOutput; + render(output: ICellOutputViewModel, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI | undefined): IRenderOutput; } export interface CellFindMatch { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index d0e14035..1025ccaf 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -20,13 +20,13 @@ import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { EditorOptions, IEditorInput, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; -import { IBorrowValue, INotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetService'; import { INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { IEditorDropService } from 'vs/workbench/services/editor/browser/editorDropService'; import { IEditorGroup, IEditorGroupsService, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { IBorrowValue, INotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetService'; const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; @@ -64,14 +64,7 @@ export class NotebookEditor extends EditorPane { this._editorMemento = this.getEditorMemento(_editorGroupService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); } - set viewModel(newModel: NotebookViewModel | undefined) { - if (this._widget.value) { - this._widget.value.viewModel = newModel; - this._onDidChangeModel.fire(); - } - } - - get viewModel() { + get viewModel(): NotebookViewModel | undefined { return this._widget.value?.viewModel; } @@ -134,17 +127,18 @@ export class NotebookEditor extends EditorPane { this._widget.value?.focus(); } + hasFocus(): boolean { + const activeElement = document.activeElement; + const value = this._widget.value; + + return !!value && (DOM.isAncestor(activeElement, value.getDomNode() || DOM.isAncestor(activeElement, value.getOverflowContainerDomNode()))); + } + async setInput(input: NotebookEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { const group = this.group!; this._saveEditorViewState(this.input); - await super.setInput(input, options, context, token); - - // Check for cancellation - if (token.isCancellationRequested) { - return undefined; - } this._widgetDisposableStore.clear(); @@ -155,11 +149,16 @@ export class NotebookEditor extends EditorPane { } this._widget = this.instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, group, input); + this._widgetDisposableStore.add(this._widget.value!.onDidChangeModel(() => this._onDidChangeModel.fire())); if (this._dimension) { this._widget.value!.layout(this._dimension, this._rootElement); } + // only now `setInput` and yield/await. this is AFTER the actual widget is ready. This is very important + // so that others synchronously receive a notebook editor with the correct widget being set + await super.setInput(input, options, context, token); + const model = await input.resolve(); // Check for cancellation if (token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index b58b545f..90204097 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getZoomLevel } from 'vs/base/browser/browser'; +import { getPixelRatio, getZoomLevel } from 'vs/base/browser/browser'; import * as DOM from 'vs/base/browser/dom'; import * as strings from 'vs/base/common/strings'; import { IMouseWheelEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; @@ -40,9 +40,8 @@ import { EditorMemento } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorMemento } from 'vs/workbench/common/editor'; import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { PANEL_BORDER } from 'vs/workbench/common/theme'; -import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugToolBar'; -import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_MARGIN, CELL_RUN_GUTTER, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellEditState, CellFocusMode, IActiveNotebookEditor, ICellViewModel, INotebookCellList, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorMouseEvent, NotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_MARGIN, CELL_OUTPUT_PADDING, CELL_RUN_GUTTER, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CellEditState, CellFocusMode, IActiveNotebookEditor, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IDisplayOutputViewModel, IGenericCellViewModel, IInsetRenderOutput, INotebookCellList, INotebookCellOutputLayoutInfo, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorMouseEvent, NotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { NotebookKernelProviderAssociation, NotebookKernelProviderAssociations, notebookKernelProviderAssociationsSettingId } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation'; import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; @@ -55,13 +54,16 @@ import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewMod import { NotebookEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { CellViewModel, IModelDecorationsChangeAccessor, INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellKind, CellToolbarLocKey, ICellRange, IInsetRenderOutput, INotebookDecorationRenderOptions, INotebookKernelInfo2, IProcessedOutput, isTransformedDisplayOutput, NotebookCellRunState, NotebookRunState, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellToolbarLocKey, ICellRange, INotebookDecorationRenderOptions, INotebookKernelInfo2, NotebookCellRunState, NotebookRunState, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { editorGutterModifiedBackground } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { configureKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { configureKernelIcon, errorStateIcon, successStateIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugColors'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { extname } from 'vs/base/common/resources'; const $ = DOM.$; @@ -73,9 +75,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private _overlayContainer!: HTMLElement; private _body!: HTMLElement; private _overflowContainer!: HTMLElement; - private _webview: BackLayerWebView | null = null; + private _webview: BackLayerWebView | null = null; private _webviewResolved: boolean = false; - private _webviewResolvePromise: Promise | null = null; + private _webviewResolvePromise: Promise | null> | null = null; private _webviewTransparentCover: HTMLElement | null = null; private _list!: INotebookCellList; private _dndController: CellDragAndDropController | null = null; @@ -146,6 +148,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._notebookViewModel?.notebookDocument; } + private _activeKernelExecuted: boolean = false; private _activeKernel: INotebookKernelInfo2 | undefined = undefined; private readonly _onDidChangeKernel = this._register(new Emitter()); readonly onDidChangeKernel: Event = this._onDidChangeKernel.event; @@ -153,6 +156,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor readonly onDidChangeAvailableKernels: Event = this._onDidChangeAvailableKernels.event; private _contributedKernelsComputePromise: CancelablePromise | null = null; + private _initialKernelComputationDone: boolean = false; get activeKernel() { return this._activeKernel; @@ -175,9 +179,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._activeKernelResolvePromise = undefined; const memento = this._activeKernelMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); - memento[this.viewModel.viewType] = this._activeKernel?.id; + memento[this.viewModel.viewType] = this._activeKernel?.friendlyId; this._activeKernelMemento.saveMemento(); this._onDidChangeKernel.fire(); + if (this._activeKernel) { + this._loadKernelPreloads(this._activeKernel.extensionLocation, this._activeKernel); + } } private _activeKernelResolvePromise: Promise | undefined = undefined; @@ -248,7 +255,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor @IContextMenuService private readonly contextMenuService: IContextMenuService, @IMenuService private readonly menuService: IMenuService, @IQuickInputService private readonly quickInputService: IQuickInputService, - @IThemeService private readonly themeService: IThemeService + @IThemeService private readonly themeService: IThemeService, + @ITelemetryService private readonly telemetryService: ITelemetryService ) { super(); this.isEmbedded = creationOptions.isEmbedded || false; @@ -428,7 +436,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private _generateFontInfo(): void { const editorOptions = this.configurationService.getValue('editor'); - this._fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()); + this._fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel(), getPixelRatio()); } private _createBody(parent: HTMLElement): void { @@ -653,6 +661,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor if (this.viewModel === undefined || !this.viewModel.equal(textModel)) { this._detachModel(); await this._attachModel(textModel, viewState); + + type WorkbenchNotebookOpenClassification = { + scheme: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; + ext: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; + viewType: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; + }; + + type WorkbenchNotebookOpenEvent = { + scheme: string; + ext: string; + viewType: string; + }; + + this.telemetryService.publicLog2('notebook/editorOpened', { + scheme: textModel.uri.scheme, + ext: extname(textModel.uri), + viewType: textModel.viewType + }); } else { this.restoreListViewState(viewState); } @@ -730,6 +756,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._webview?.element.remove(); this._webview = null; this._list.clear(); + this._activeKernel = undefined; + this._activeKernelExecuted = false; } async beginComputeContributedKernels() { @@ -742,6 +770,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor }); const result = await this._contributedKernelsComputePromise; + this._initialKernelComputationDone = true; this._contributedKernelsComputePromise = null; return result; @@ -752,6 +781,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } + if (this._activeKernel !== undefined && this._activeKernelExecuted) { + // kernel already executed, we should not change it automatically + return; + } + const provider = this.notebookService.getContributedNotebookProvider(this.viewModel.viewType) || this.notebookService.getContributedNotebookProviders(this.viewModel.uri)[0]; const availableKernels = await this.beginComputeContributedKernels(); @@ -771,7 +805,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.multipleKernelsAvailable = false; } - const activeKernelStillExist = [...availableKernels].find(kernel => kernel.id === this.activeKernel?.id && this.activeKernel?.id !== undefined); + const activeKernelStillExist = [...availableKernels].find(kernel => kernel.friendlyId === this.activeKernel?.friendlyId && this.activeKernel?.friendlyId !== undefined); if (activeKernelStillExist) { // the kernel still exist, we don't want to modify the selection otherwise user's temporary preference is lost @@ -782,6 +816,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._setKernelsFromProviders(provider, availableKernels, tokenSource); } + this._initialKernelComputationDone = true; + tokenSource.dispose(); } @@ -797,7 +833,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const cachedKernelId = memento[provider.id]; this.activeKernel = filteredKernels.find(kernel => kernel.isPreferred) - || filteredKernels.find(kernel => kernel.id === cachedKernelId) + || filteredKernels.find(kernel => kernel.friendlyId === cachedKernelId) || filteredKernels[0]; } else { this.activeKernel = undefined; @@ -818,7 +854,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - memento[provider.id] = this._activeKernel?.id; + memento[provider.id] = this._activeKernel?.friendlyId; this._activeKernelMemento.saveMemento(); tokenSource.dispose(); @@ -831,7 +867,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const cachedKernelId = memento[provider.id]; const preferedKernel = kernelsFromSameExtension.find(kernel => kernel.isPreferred) - || kernelsFromSameExtension.find(kernel => kernel.id === cachedKernelId) + || kernelsFromSameExtension.find(kernel => kernel.friendlyId === cachedKernelId) || kernelsFromSameExtension[0]; this.activeKernel = preferedKernel; if (this.activeKernel) { @@ -848,7 +884,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } - memento[provider.id] = this._activeKernel?.id; + memento[provider.id] = this._activeKernel?.friendlyId; this._activeKernelMemento.saveMemento(); tokenSource.dispose(); return; @@ -892,7 +928,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._notebookExecuting?.set(notebookMetadata.runState === NotebookRunState.Running); } - private async _resolveWebview(): Promise { + private async _resolveWebview(): Promise | null> { if (!this.textModel) { return null; } @@ -902,7 +938,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } if (!this._webview) { - this._webview = this.instantiationService.createInstance(BackLayerWebView, this, this.getId(), this.textModel!.uri); + this._webview = this.instantiationService.createInstance(BackLayerWebView, this, this.getId(), this.textModel!.uri, { outputNodePadding: CELL_OUTPUT_PADDING, outputNodeLeftPadding: CELL_OUTPUT_PADDING }); + this._webview.element.style.width = `calc(100% - ${CODE_CELL_LEFT_MARGIN + (CELL_MARGIN * 2) + CELL_RUN_GUTTER}px)`; + this._webview.element.style.margin = `0px 0 0px ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px`; + // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._webview.element); } @@ -950,7 +989,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } private async _createWebview(id: string, resource: URI): Promise { - this._webview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource); + this._webview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, { outputNodePadding: CELL_OUTPUT_PADDING, outputNodeLeftPadding: CELL_OUTPUT_PADDING }); + this._webview.element.style.width = `calc(100% - ${CODE_CELL_LEFT_MARGIN + (CELL_MARGIN * 2) + CELL_RUN_GUTTER}px)`; + this._webview.element.style.margin = `0px 0 0px ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px`; // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._webview.element); } @@ -1016,27 +1057,36 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._webview!.element.style.height = `${scrollHeight}px`; if (this._webview?.insetMapping) { - const updateItems: { cell: CodeCellViewModel, output: IProcessedOutput, cellTop: number }[] = []; - const removedItems: IProcessedOutput[] = []; + const updateItems: IDisplayOutputLayoutUpdateRequest[] = []; + const removedItems: IDisplayOutputViewModel[] = []; this._webview?.insetMapping.forEach((value, key) => { - const cell = value.cell; + const cell = this.viewModel?.getCellByHandle(value.cellInfo.cellHandle); + if (!cell || !(cell instanceof CodeCellViewModel)) { + return; + } + + this.viewModel?.viewCells.find(cell => cell.handle === value.cellInfo.cellHandle); const viewIndex = this._list.getViewIndex(cell); if (viewIndex === undefined) { return; } - if (cell.outputs.indexOf(key) < 0) { + if (cell.outputsViewModels.indexOf(key) < 0) { // output is already gone removedItems.push(key); } const cellTop = this._list.getAbsoluteTopOfElement(cell); if (this._webview!.shouldUpdateInset(cell, key, cellTop)) { + const outputIndex = cell.outputsViewModels.indexOf(key); + + const outputOffset = cellTop + cell.getOutputOffset(outputIndex); + updateItems.push({ - cell: cell, output: key, - cellTop: cellTop + cellTop: cellTop, + outputOffset }); } }); @@ -1212,10 +1262,18 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor // this.viewModel!.selectionHandles = [cell.handle]; } + revealCellRangeInView(range: ICellRange) { + return this._list.revealElementsInView(range); + } + revealInView(cell: ICellViewModel) { this._list.revealElementInView(cell); } + revealInViewAtTop(cell: ICellViewModel) { + this._list.revealElementInViewAtTop(cell); + } + revealInCenterIfOutsideViewport(cell: ICellViewModel) { this._list.revealElementInCenterIfOutsideViewport(cell); } @@ -1421,6 +1479,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor language = this.viewModel.resolvedLanguages[0] || 'plaintext'; } } + + if (this.viewModel.resolvedLanguages.indexOf(language) < 0) { + // the language no longer exists + language = this.viewModel.resolvedLanguages[0] || 'plaintext'; + } } else { language = 'markdown'; } @@ -1627,26 +1690,37 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private async _ensureActiveKernel() { if (this._activeKernel) { - if (this._activeKernelResolvePromise) { - await this._activeKernelResolvePromise; - } - return; } + if (this._activeKernelResolvePromise) { + await this._activeKernelResolvePromise; + + if (this._activeKernel) { + return; + } + } + + + if (!this._initialKernelComputationDone) { + await this._setKernels(new CancellationTokenSource()); + + if (this._activeKernel) { + return; + } + } + // pick active kernel const picker = this.quickInputService.createQuickPick<(IQuickPickItem & { run(): void; kernelProviderId?: string })>(); picker.placeholder = nls.localize('notebook.runCell.selectKernel', "Select a notebook kernel to run this notebook"); picker.matchOnDetail = true; - picker.busy = true; - picker.show(); const tokenSource = new CancellationTokenSource(); const availableKernels = await this.beginComputeContributedKernels(); const picks: QuickPickInput[] = availableKernels.map((a) => { return { - id: a.id, + id: a.friendlyId, label: a.label, picked: false, description: @@ -1736,6 +1810,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } await this._ensureActiveKernel(); + this._activeKernelExecuted = true; await this._activeKernel?.executeNotebookCell!(this.viewModel.uri, undefined); } @@ -1776,6 +1851,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } await this._ensureActiveKernel(); + this._activeKernelExecuted = true; await this._activeKernel?.executeNotebookCell!(this.viewModel.uri, cell.handle); } @@ -1818,6 +1894,20 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } + focusNextNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output') { + const idx = this.viewModel?.getCellIndex(cell); + if (typeof idx !== 'number') { + return; + } + + const newCell = this.viewModel?.viewCells[idx + 1]; + if (!newCell) { + return; + } + + this.focusNotebookCell(newCell, focusItem); + } + //#endregion //#region MISC @@ -1842,12 +1932,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor }; } + getCellOutputLayoutInfo(cell: IGenericCellViewModel): INotebookCellOutputLayoutInfo { + if (!this._list) { + throw new Error('Editor is not initalized successfully'); + } + + return { + width: this._dimension!.width, + height: this._dimension!.height, + fontInfo: this._fontInfo! + }; + } + triggerScroll(event: IMouseWheelEvent) { this._list.triggerScrollFromMouseWheelEvent(event); } async createInset(cell: CodeCellViewModel, output: IInsetRenderOutput, offset: number): Promise { - this._insetModifyQueueByOutputId.queue(output.source.outputId, async () => { + this._insetModifyQueueByOutputId.queue(output.source.model.outputId, async () => { if (!this._webview) { return; } @@ -1856,22 +1958,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor if (!this._webview!.insetMapping.has(output.source)) { const cellTop = this._list.getAbsoluteTopOfElement(cell); - await this._webview!.createInset(cell, output, cellTop, offset); + await this._webview!.createInset({ cellId: cell.id, cellHandle: cell.handle, cellUri: cell.uri }, output, cellTop, offset); } else { const cellTop = this._list.getAbsoluteTopOfElement(cell); const scrollTop = this._list.scrollTop; + const outputIndex = cell.outputsViewModels.indexOf(output.source); + const outputOffset = cellTop + cell.getOutputOffset(outputIndex); - this._webview!.updateViewScrollTop(-scrollTop, true, [{ cell, output: output.source, cellTop }]); + this._webview!.updateViewScrollTop(-scrollTop, true, [{ output: output.source, cellTop, outputOffset }]); } }); } - removeInset(output: IProcessedOutput) { - if (!isTransformedDisplayOutput(output)) { + removeInset(output: ICellOutputViewModel) { + if (!output.isDisplayOutput()) { return; } - this._insetModifyQueueByOutputId.queue(output.outputId, async () => { + this._insetModifyQueueByOutputId.queue(output.model.outputId, async () => { if (!this._webview || !this._webviewResolved) { return; } @@ -1879,16 +1983,16 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor }); } - hideInset(output: IProcessedOutput) { + hideInset(output: ICellOutputViewModel) { if (!this._webview || !this._webviewResolved) { return; } - if (!isTransformedDisplayOutput(output)) { + if (!output.isDisplayOutput()) { return; } - this._insetModifyQueueByOutputId.queue(output.outputId, async () => { + this._insetModifyQueueByOutputId.queue(output.model.outputId, async () => { this._webview!.hideInset(output); }); } @@ -1921,6 +2025,20 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._overlayContainer.classList.remove(className); } + getCellByInfo(cellInfo: ICommonCellInfo): ICellViewModel { + const { cellHandle } = cellInfo; + return this.viewModel?.viewCells.find(vc => vc.handle === cellHandle) as CodeCellViewModel; + } + + updateOutputHeight(cellInfo: ICommonCellInfo, output: IDisplayOutputViewModel, outputHeight: number, isInit: boolean): void { + const cell = this.viewModel?.viewCells.find(vc => vc.handle === cellInfo.cellHandle); + if (cell && cell instanceof CodeCellViewModel) { + const outputIndex = cell.outputsViewModels.indexOf(output); + cell.updateOutputHeight(outputIndex, outputHeight); + this.layoutNotebookCell(cell, cell.layoutInfo.totalHeight); + } + } + //#endregion @@ -2022,7 +2140,7 @@ export const selectedCellBorder = registerColor('notebook.selectedCellBorder', { dark: notebookCellBorder, light: notebookCellBorder, hc: contrastBorder -}, nls.localize('notebook.selectedCellBorder', "The color of the cell's top and bottom border when the cell is selected but not focusd.")); +}, nls.localize('notebook.selectedCellBorder', "The color of the cell's top and bottom border when the cell is selected but not focused.")); export const focusedCellBorder = registerColor('notebook.focusedCellBorder', { dark: focusBorder, @@ -2030,6 +2148,12 @@ export const focusedCellBorder = registerColor('notebook.focusedCellBorder', { hc: focusBorder }, nls.localize('notebook.focusedCellBorder', "The color of the cell's top and bottom border when the cell is focused.")); +export const inactiveFocusedCellBorder = registerColor('notebook.inactiveFocusedCellBorder', { + dark: notebookCellBorder, + light: notebookCellBorder, + hc: notebookCellBorder +}, nls.localize('notebook.inactiveFocusedCellBorder', "The color of the cell's top and bottom border when a cell is focused while the primary focus is outside of the editor.")); + export const cellStatusBarItemHover = registerColor('notebook.cellStatusBarItemHoverBackground', { light: new Color(new RGBA(0, 0, 0, 0.08)), dark: new Color(new RGBA(255, 255, 255, 0.15)), @@ -2042,7 +2166,6 @@ export const cellInsertionIndicator = registerColor('notebook.cellInsertionIndic hc: focusBorder }, nls.localize('notebook.cellInsertionIndicator', "The color of the notebook cell insertion indicator.")); - export const listScrollbarSliderBackground = registerColor('notebookScrollbarSlider.background', { dark: scrollbarSliderBackground, light: scrollbarSliderBackground, @@ -2161,6 +2284,15 @@ registerThemingParticipant((theme, collector) => { border-color: ${focusedCellBorderColor} !important; }`); + const inactiveFocusedBorderColor = theme.getColor(inactiveFocusedCellBorder); + collector.addRule(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-top:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-bottom:before, + .monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row.focused .cell-inner-container:not(.cell-editor-focus):before, + .monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row.focused .cell-inner-container:not(.cell-editor-focus):after { + border-color: ${inactiveFocusedBorderColor} !important; + }`); + const selectedCellBorderColor = theme.getColor(selectedCellBorder); collector.addRule(` .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-editor-focus .cell-focus-indicator-top:before, @@ -2191,12 +2323,12 @@ registerThemingParticipant((theme, collector) => { const cellStatusSuccessIcon = theme.getColor(cellStatusIconSuccess); if (cellStatusSuccessIcon) { - collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-status .codicon-notebook-state-success { color: ${cellStatusSuccessIcon} }`); + collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-status ${ThemeIcon.asCSSSelector(successStateIcon)} { color: ${cellStatusSuccessIcon} }`); } const cellStatusErrorIcon = theme.getColor(cellStatusIconError); if (cellStatusErrorIcon) { - collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-status .codicon-notebook-state-error { color: ${cellStatusErrorIcon} }`); + collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-status ${ThemeIcon.asCSSSelector(errorStateIcon)} { color: ${cellStatusErrorIcon} }`); } const cellStatusRunningIcon = theme.getColor(cellStatusIconRunning); @@ -2219,12 +2351,14 @@ registerThemingParticipant((theme, collector) => { if (scrollbarSliderBackgroundColor) { collector.addRule(` .notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar > .slider { background: ${editorBackgroundColor}; } `); collector.addRule(` .notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar > .slider:before { content: ""; width: 100%; height: 100%; position: absolute; background: ${scrollbarSliderBackgroundColor}; } `); /* hack to not have cells see through scroller */ + // collector.addRule(` .monaco-workbench .notebookOverlay .output-plaintext::-webkit-scrollbar-track { background: ${scrollbarSliderBackgroundColor}; } `); } const scrollbarSliderHoverBackgroundColor = theme.getColor(listScrollbarSliderHoverBackground); if (scrollbarSliderHoverBackgroundColor) { collector.addRule(` .notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar > .slider:hover { background: ${editorBackgroundColor}; } `); collector.addRule(` .notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar > .slider:hover:before { content: ""; width: 100%; height: 100%; position: absolute; background: ${scrollbarSliderHoverBackgroundColor}; } `); /* hack to not have cells see through scroller */ + collector.addRule(` .monaco-workbench .notebookOverlay .output-plaintext::-webkit-scrollbar-thumb { background: ${scrollbarSliderHoverBackgroundColor}; } `); } const scrollbarSliderActiveBackgroundColor = theme.getColor(listScrollbarSliderActiveBackground); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts index f9650ad0..ce817ad4 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts @@ -3,14 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ResourceMap } from 'vs/base/common/map'; import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { IEditorGroupsService, IEditorGroup, GroupChangeKind, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IInstantiationService, createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export const INotebookEditorWidgetService = createDecorator('INotebookEditorWidgetService'); @@ -20,139 +16,6 @@ export interface IBorrowValue { export interface INotebookEditorWidgetService { _serviceBrand: undefined; + widgets: NotebookEditorWidget[]; retrieveWidget(accessor: ServicesAccessor, group: IEditorGroup, input: NotebookEditorInput): IBorrowValue; } - -class NotebookEditorWidgetService implements INotebookEditorWidgetService { - - readonly _serviceBrand: undefined; - - private _tokenPool = 1; - - private readonly _notebookWidgets = new Map>(); - private readonly _disposables = new DisposableStore(); - - constructor( - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IEditorService editorService: IEditorService, - ) { - - const groupListener = new Map(); - const onNewGroup = (group: IEditorGroup) => { - const { id } = group; - const listener = group.onDidGroupChange(e => { - const widgets = this._notebookWidgets.get(group.id); - if (!widgets || e.kind !== GroupChangeKind.EDITOR_CLOSE || !(e.editor instanceof NotebookEditorInput)) { - return; - } - const value = widgets.get(e.editor.resource); - if (!value) { - return; - } - value.token = undefined; - this._disposeWidget(value.widget); - widgets.delete(e.editor.resource); - }); - groupListener.set(id, listener); - }; - this._disposables.add(editorGroupService.onDidAddGroup(onNewGroup)); - editorGroupService.groups.forEach(onNewGroup); - - // group removed -> clean up listeners, clean up widgets - this._disposables.add(editorGroupService.onDidRemoveGroup(group => { - const listener = groupListener.get(group.id); - if (listener) { - listener.dispose(); - groupListener.delete(group.id); - } - const widgets = this._notebookWidgets.get(group.id); - this._notebookWidgets.delete(group.id); - if (widgets) { - for (const value of widgets.values()) { - value.token = undefined; - this._disposeWidget(value.widget); - } - } - })); - - // HACK - // we use the open override to spy on tab movements because that's the only - // way to do that... - this._disposables.add(editorService.overrideOpenEditor({ - open: (input, _options, group, context) => { - if (input instanceof NotebookEditorInput && context === OpenEditorContext.MOVE_EDITOR) { - // when moving a notebook editor we release it from its current tab and we - // "place" it into its future slot so that the editor can pick it up from there - this._freeWidget(input, editorGroupService.activeGroup, group); - } - return undefined; - } - })); - } - - private _disposeWidget(widget: NotebookEditorWidget): void { - widget.onWillHide(); - const domNode = widget.getDomNode(); - widget.dispose(); - domNode.remove(); - } - - private _freeWidget(input: NotebookEditorInput, source: IEditorGroup, target: IEditorGroup): void { - const targetWidget = this._notebookWidgets.get(target.id)?.get(input.resource); - if (targetWidget) { - // not needed - return; - } - - const widget = this._notebookWidgets.get(source.id)?.get(input.resource); - if (!widget) { - throw new Error('no widget at source group'); - } - this._notebookWidgets.get(source.id)?.delete(input.resource); - widget.token = undefined; - - let targetMap = this._notebookWidgets.get(target.id); - if (!targetMap) { - targetMap = new ResourceMap(); - this._notebookWidgets.set(target.id, targetMap); - } - targetMap.set(input.resource, widget); - } - - retrieveWidget(accessor: ServicesAccessor, group: IEditorGroup, input: NotebookEditorInput): IBorrowValue { - - let value = this._notebookWidgets.get(group.id)?.get(input.resource); - - if (!value) { - // NEW widget - const instantiationService = accessor.get(IInstantiationService); - const widget = instantiationService.createInstance(NotebookEditorWidget, { isEmbedded: false }); - const token = this._tokenPool++; - value = { widget, token }; - - let map = this._notebookWidgets.get(group.id); - if (!map) { - map = new ResourceMap(); - this._notebookWidgets.set(group.id, map); - } - map.set(input.resource, value); - - } else { - // reuse a widget which was either free'ed before or which - // is simply being reused... - value.token = this._tokenPool++; - } - - return this._createBorrowValue(value.token!, value); - } - - private _createBorrowValue(myToken: number, widget: { widget: NotebookEditorWidget, token: number | undefined }): IBorrowValue { - return { - get value() { - return widget.token === myToken ? widget.widget : undefined; - } - }; - } -} - -registerSingleton(INotebookEditorWidgetService, NotebookEditorWidgetService, true); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetServiceImpl.ts new file mode 100644 index 00000000..f365cdfd --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetServiceImpl.ts @@ -0,0 +1,149 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ResourceMap } from 'vs/base/common/map'; +import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { IEditorGroupsService, IEditorGroup, GroupChangeKind, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IBorrowValue, INotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetService'; + +export class NotebookEditorWidgetService implements INotebookEditorWidgetService { + + readonly _serviceBrand: undefined; + + private _tokenPool = 1; + + private readonly _notebookWidgets = new Map>(); + private readonly _disposables = new DisposableStore(); + + get widgets() { + return [...this._notebookWidgets.values()].map(val => [...val.values()].map(widget => widget.widget)).reduce((prev, curr) => { return [...prev, ...curr]; }, []); + } + + constructor( + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IEditorService editorService: IEditorService, + ) { + + const groupListener = new Map(); + const onNewGroup = (group: IEditorGroup) => { + const { id } = group; + const listener = group.onDidGroupChange(e => { + const widgets = this._notebookWidgets.get(group.id); + if (!widgets || e.kind !== GroupChangeKind.EDITOR_CLOSE || !(e.editor instanceof NotebookEditorInput)) { + return; + } + const value = widgets.get(e.editor.resource); + if (!value) { + return; + } + value.token = undefined; + this._disposeWidget(value.widget); + widgets.delete(e.editor.resource); + }); + groupListener.set(id, listener); + }; + this._disposables.add(editorGroupService.onDidAddGroup(onNewGroup)); + editorGroupService.groups.forEach(onNewGroup); + + // group removed -> clean up listeners, clean up widgets + this._disposables.add(editorGroupService.onDidRemoveGroup(group => { + const listener = groupListener.get(group.id); + if (listener) { + listener.dispose(); + groupListener.delete(group.id); + } + const widgets = this._notebookWidgets.get(group.id); + this._notebookWidgets.delete(group.id); + if (widgets) { + for (const value of widgets.values()) { + value.token = undefined; + this._disposeWidget(value.widget); + } + } + })); + + // HACK + // we use the open override to spy on tab movements because that's the only + // way to do that... + this._disposables.add(editorService.overrideOpenEditor({ + open: (input, _options, group, context) => { + if (input instanceof NotebookEditorInput && context === OpenEditorContext.MOVE_EDITOR) { + // when moving a notebook editor we release it from its current tab and we + // "place" it into its future slot so that the editor can pick it up from there + this._freeWidget(input, editorGroupService.activeGroup, group); + } + return undefined; + } + })); + } + + private _disposeWidget(widget: NotebookEditorWidget): void { + widget.onWillHide(); + const domNode = widget.getDomNode(); + widget.dispose(); + domNode.remove(); + } + + private _freeWidget(input: NotebookEditorInput, source: IEditorGroup, target: IEditorGroup): void { + const targetWidget = this._notebookWidgets.get(target.id)?.get(input.resource); + if (targetWidget) { + // not needed + return; + } + + const widget = this._notebookWidgets.get(source.id)?.get(input.resource); + if (!widget) { + throw new Error('no widget at source group'); + } + this._notebookWidgets.get(source.id)?.delete(input.resource); + widget.token = undefined; + + let targetMap = this._notebookWidgets.get(target.id); + if (!targetMap) { + targetMap = new ResourceMap(); + this._notebookWidgets.set(target.id, targetMap); + } + targetMap.set(input.resource, widget); + } + + retrieveWidget(accessor: ServicesAccessor, group: IEditorGroup, input: NotebookEditorInput): IBorrowValue { + + let value = this._notebookWidgets.get(group.id)?.get(input.resource); + + if (!value) { + // NEW widget + const instantiationService = accessor.get(IInstantiationService); + const widget = instantiationService.createInstance(NotebookEditorWidget, { isEmbedded: false }); + const token = this._tokenPool++; + value = { widget, token }; + + let map = this._notebookWidgets.get(group.id); + if (!map) { + map = new ResourceMap(); + this._notebookWidgets.set(group.id, map); + } + map.set(input.resource, value); + + } else { + // reuse a widget which was either free'ed before or which + // is simply being reused... + value.token = this._tokenPool++; + } + + return this._createBorrowValue(value.token!, value); + } + + private _createBorrowValue(myToken: number, widget: { widget: NotebookEditorWidget, token: number | undefined }): IBorrowValue { + return { + get value() { + return widget.token === myToken ? widget.widget : undefined; + } + }; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts index 6780f1fa..0ff8cae8 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts @@ -29,4 +29,5 @@ export const collapsedIcon = registerIcon('notebook-collapsed', Codicon.chevronR export const expandedIcon = registerIcon('notebook-expanded', Codicon.chevronDown, localize('expandedIcon', 'Icon to annotated a expanded section in notebook editors.')); export const openAsTextIcon = registerIcon('notebook-open-as-text', Codicon.fileCode, localize('openAsTextIcon', 'Icon to open the notebook in a text editor.')); export const revertIcon = registerIcon('notebook-revert', Codicon.discard, localize('revertIcon', 'Icon to revert in notebook editors.')); +export const renderOutputIcon = registerIcon('notebook-render-output', Codicon.preview, localize('renderOutputIcon', 'Icon to render output in diff editor.')); export const mimetypeIcon = registerIcon('notebook-mimetype', Codicon.code, localize('mimetypeIcon', 'Icon for a mime type in notebook editors.')); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts b/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts index abf2220c..e97746c1 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts @@ -5,9 +5,9 @@ import { CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { BrandedService, IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation'; -import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICommonNotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -export type IOutputTransformCtor = IConstructorSignature1; +export type IOutputTransformCtor = IConstructorSignature1; export interface IOutputTransformDescription { id: string; @@ -20,7 +20,7 @@ export const NotebookRegistry = new class NotebookRegistryImpl { readonly outputTransforms: IOutputTransformDescription[] = []; - registerOutputTransform(id: string, kind: CellOutputKind, ctor: { new(editor: INotebookEditor, ...services: Services): IOutputTransformContribution }): void { + registerOutputTransform(id: string, kind: CellOutputKind, ctor: { new(editor: ICommonNotebookEditor, ...services: Services): IOutputTransformContribution }): void { this.outputTransforms.push({ id: id, kind: kind, ctor: ctor as IOutputTransformCtor }); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index ec00de56..6209a04e 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getZoomLevel } from 'vs/base/browser/browser'; +import { getPixelRatio, getZoomLevel } from 'vs/base/browser/browser'; import { flatten } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; @@ -65,6 +65,10 @@ export class NotebookKernelProviderInfoStore extends Disposable { return this._notebookKernelProviders.filter(provider => notebookDocumentFilterMatch(provider.selector, viewType, resource)); } + getContributedKernelProviders() { + return [...this._notebookKernelProviders.values()]; + } + private _updateProviderExtensionsInfo() { NotebookKernelProviderAssociationRegistry.extensionIds.length = 0; NotebookKernelProviderAssociationRegistry.extensionDescriptions.length = 0; @@ -237,7 +241,7 @@ class ModelData implements IDisposable { } export class NotebookService extends Disposable implements INotebookService, ICustomEditorViewTypesHandler { declare readonly _serviceBrand: undefined; - private readonly _notebookProviders = new Map(); + private readonly _notebookProviders = new Map(); notebookProviderInfoStore: NotebookProviderInfoStore; notebookRenderersInfoStore: NotebookOutputRendererInfoStore = new NotebookOutputRendererInfoStore(); notebookKernelProviderInfoStore: NotebookKernelProviderInfoStore = new NotebookKernelProviderInfoStore(); @@ -266,12 +270,12 @@ export class NotebookService extends Disposable implements INotebookService, ICu private readonly _onDidChangeKernels = new Emitter(); onDidChangeKernels: Event = this._onDidChangeKernels.event; - private readonly _onDidChangeNotebookActiveKernel = new Emitter<{ uri: URI, providerHandle: number | undefined, kernelId: string | undefined }>(); - onDidChangeNotebookActiveKernel: Event<{ uri: URI, providerHandle: number | undefined, kernelId: string | undefined }> = this._onDidChangeNotebookActiveKernel.event; + private readonly _onDidChangeNotebookActiveKernel = new Emitter<{ uri: URI, providerHandle: number | undefined, kernelFriendlyId: string | undefined; }>(); + onDidChangeNotebookActiveKernel: Event<{ uri: URI, providerHandle: number | undefined, kernelFriendlyId: string | undefined; }> = this._onDidChangeNotebookActiveKernel.event; private cutItems: NotebookCellTextModel[] | undefined; private _lastClipboardIsCopy: boolean = true; - private _displayOrder: { userOrder: string[], defaultOrder: string[] } = Object.create(null); + private _displayOrder: { userOrder: string[], defaultOrder: string[]; } = Object.create(null); private readonly _decorationOptionProviders = new Map(); constructor( @@ -366,7 +370,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu // there is a `::before` or `::after` text decoration whose position is above or below current line // we at least make sure that the editor top padding is at least one line const editorOptions = this.configurationService.getValue('editor'); - updateEditorTopPadding(BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()).lineHeight + 2); + updateEditorTopPadding(BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel(), getPixelRatio()).lineHeight + 2); decorationTriggeredAdjustment = true; break; } @@ -387,7 +391,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu }; }; - const PRIORITY = 50; + const PRIORITY = 105; this._register(UndoCommand.addImplementation(PRIORITY, () => { const { editor } = getContext(); if (editor?.viewModel) { @@ -623,6 +627,8 @@ export class NotebookService extends Disposable implements INotebookService, ICu } async canResolve(viewType: string): Promise { + await this._extensionService.activateByEvent(`onNotebook:*`); + if (!this._notebookProviders.has(viewType)) { await this._extensionService.whenInstalledExtensionsRegistered(); // this awaits full activation of all matching extensions @@ -691,9 +697,10 @@ export class NotebookService extends Disposable implements INotebookService, ICu const data = await provider.provideKernels(resource, token); result[index] = data.map(dto => { return { + id: dto.id, extension: dto.extension, extensionLocation: URI.revive(dto.extensionLocation), - id: dto.id, + friendlyId: dto.friendlyId, label: dto.label, description: dto.description, detail: dto.detail, @@ -701,13 +708,13 @@ export class NotebookService extends Disposable implements INotebookService, ICu preloads: dto.preloads, providerHandle: dto.providerHandle, resolve: async (uri: URI, editorId: string, token: CancellationToken) => { - return provider.resolveKernel(editorId, uri, dto.id, token); + return provider.resolveKernel(editorId, uri, dto.friendlyId, token); }, executeNotebookCell: async (uri: URI, handle: number | undefined) => { - return provider.executeNotebook(uri, dto.id, handle); + return provider.executeNotebook(uri, dto.friendlyId, handle); }, cancelNotebookCell: (uri: URI, handle: number | undefined): Promise => { - return provider.cancelNotebook(uri, dto.id, handle); + return provider.cancelNotebook(uri, dto.friendlyId, handle); } }; }); @@ -718,6 +725,11 @@ export class NotebookService extends Disposable implements INotebookService, ICu return flatten(result); } + async getContributedNotebookKernelProviders(): Promise { + const kernelProviders = this.notebookKernelProviderInfoStore.getContributedKernelProviders(); + return kernelProviders; + } + getRendererInfo(id: string): INotebookRendererInfo | undefined { return this.notebookRenderersInfoStore.get(id); } @@ -890,7 +902,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu listVisibleNotebookEditors(): INotebookEditor[] { return this._editorService.visibleEditorPanes - .filter(pane => (pane as unknown as { isNotebookEditor?: boolean }).isNotebookEditor) + .filter(pane => (pane as unknown as { isNotebookEditor?: boolean; }).isNotebookEditor) .map(pane => pane.getControl() as INotebookEditor) .filter(editor => !!editor) .filter(editor => this._notebookEditors.has(editor.getId())); @@ -912,7 +924,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu this._onDidChangeNotebookActiveKernel.fire({ uri: editor.uri!, providerHandle: editor.activeKernel?.providerHandle, - kernelId: editor.activeKernel?.id + kernelFriendlyId: editor.activeKernel?.friendlyId }); })); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index d6d8ed7c..8d0c6a54 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -19,9 +19,9 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IListService, IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/listService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { CellRevealPosition, CellRevealType, CursorAtBoundary, getVisibleCells, ICellViewModel, INotebookCellList, reduceCellRanges, CellEditState, CellFocusMode, BaseCellRenderTemplate, NOTEBOOK_CELL_LIST_FOCUSED, cellRangesEqual } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellRevealPosition, CellRevealType, CursorAtBoundary, getVisibleCells, ICellViewModel, INotebookCellList, reduceCellRanges, CellEditState, CellFocusMode, BaseCellRenderTemplate, NOTEBOOK_CELL_LIST_FOCUSED, cellRangesEqual, ICellOutputViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { diff, IProcessedOutput, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind, ICellRange, NOTEBOOK_EDITOR_CURSOR_BEGIN_END } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { diff, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind, ICellRange, NOTEBOOK_EDITOR_CURSOR_BEGIN_END } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { clamp } from 'vs/base/common/numbers'; import { SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; @@ -45,10 +45,10 @@ export class NotebookCellList extends WorkbenchList implements ID private _viewModelStore = new DisposableStore(); private styleElement?: HTMLStyleElement; - private readonly _onDidRemoveOutput = new Emitter(); - readonly onDidRemoveOutput: Event = this._onDidRemoveOutput.event; - private readonly _onDidHideOutput = new Emitter(); - readonly onDidHideOutput: Event = this._onDidHideOutput.event; + private readonly _onDidRemoveOutput = new Emitter(); + readonly onDidRemoveOutput: Event = this._onDidRemoveOutput.event; + private readonly _onDidHideOutput = new Emitter(); + readonly onDidHideOutput: Event = this._onDidHideOutput.event; private _viewModel: NotebookViewModel | null = null; private _hiddenRangeIds: string[] = []; @@ -314,15 +314,17 @@ export class NotebookCellList extends WorkbenchList implements ID if (e.synchronous) { viewDiffs.reverse().forEach((diff) => { // remove output in the webview - const hideOutputs: IProcessedOutput[] = []; - const deletedOutputs: IProcessedOutput[] = []; + const hideOutputs: ICellOutputViewModel[] = []; + const deletedOutputs: ICellOutputViewModel[] = []; for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { const cell = this.element(i); - if (this._viewModel!.hasCell(cell.handle)) { - hideOutputs.push(...cell?.model.outputs); - } else { - deletedOutputs.push(...cell?.model.outputs); + if (cell.cellKind === CellKind.Code) { + if (this._viewModel!.hasCell(cell.handle)) { + hideOutputs.push(...cell?.outputsViewModels); + } else { + deletedOutputs.push(...cell?.outputsViewModels); + } } } @@ -338,15 +340,17 @@ export class NotebookCellList extends WorkbenchList implements ID } viewDiffs.reverse().forEach((diff) => { - const hideOutputs: IProcessedOutput[] = []; - const deletedOutputs: IProcessedOutput[] = []; + const hideOutputs: ICellOutputViewModel[] = []; + const deletedOutputs: ICellOutputViewModel[] = []; for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { const cell = this.element(i); - if (this._viewModel!.hasCell(cell.handle)) { - hideOutputs.push(...cell?.model.outputs); - } else { - deletedOutputs.push(...cell?.model.outputs); + if (cell.cellKind === CellKind.Code) { + if (this._viewModel!.hasCell(cell.handle)) { + hideOutputs.push(...cell?.outputsViewModels); + } else { + deletedOutputs.push(...cell?.outputsViewModels); + } } } @@ -460,15 +464,17 @@ export class NotebookCellList extends WorkbenchList implements ID viewDiffs.reverse().forEach((diff) => { // remove output in the webview - const hideOutputs: IProcessedOutput[] = []; - const deletedOutputs: IProcessedOutput[] = []; + const hideOutputs: ICellOutputViewModel[] = []; + const deletedOutputs: ICellOutputViewModel[] = []; for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { const cell = this.element(i); - if (this._viewModel!.hasCell(cell.handle)) { - hideOutputs.push(...cell?.model.outputs); - } else { - deletedOutputs.push(...cell?.model.outputs); + if (cell.cellKind === CellKind.Code) { + if (this._viewModel!.hasCell(cell.handle)) { + hideOutputs.push(...cell?.outputsViewModels); + } else { + deletedOutputs.push(...cell?.outputsViewModels); + } } } @@ -541,6 +547,22 @@ export class NotebookCellList extends WorkbenchList implements ID return viewIndexInfo.index; } + private _getViewIndexUpperBound2(modelIndex: number) { + if (!this.hiddenRangesPrefixSum) { + return modelIndex; + } + + const viewIndexInfo = this.hiddenRangesPrefixSum.getIndexOf(modelIndex); + + if (viewIndexInfo.remainder !== 0) { + if (modelIndex >= this.hiddenRangesPrefixSum.getTotalValue()) { + return modelIndex - (this.hiddenRangesPrefixSum.getTotalValue() - this.hiddenRangesPrefixSum.getCount()); + } + } + + return viewIndexInfo.index; + } + focusElement(cell: ICellViewModel) { const index = this._getViewIndexUpperBound(cell); @@ -581,6 +603,46 @@ export class NotebookCellList extends WorkbenchList implements ID super.setFocus(indexes, browserEvent); } + revealElementsInView(range: ICellRange) { + const startIndex = this._getViewIndexUpperBound2(range.start); + + if (startIndex < 0) { + return; + } + + const endIndex = this._getViewIndexUpperBound2(range.end); + + const scrollTop = this.getViewScrollTop(); + const wrapperBottom = this.getViewScrollBottom(); + const elementTop = this.view.elementTop(startIndex); + if (elementTop >= scrollTop + && elementTop < wrapperBottom) { + // start element is visible + // check end + + const endElementTop = this.view.elementTop(endIndex); + const endElementHeight = this.view.elementHeight(endIndex); + + if (endElementTop >= wrapperBottom) { + return this._revealInternal(startIndex, false, CellRevealPosition.Top); + } + + if (endElementTop < wrapperBottom) { + // end element partially visible + if (endElementTop + endElementHeight - wrapperBottom < elementTop - scrollTop) { + // there is enough space to just scroll up a little bit to make the end element visible + return this.view.setScrollTop(scrollTop + endElementTop + endElementHeight - wrapperBottom); + } else { + // don't even try it + return this._revealInternal(startIndex, false, CellRevealPosition.Top); + } + } + } + + + this._revealInView(startIndex); + } + revealElementInView(cell: ICellViewModel) { const index = this._getViewIndexUpperBound(cell); @@ -589,6 +651,14 @@ export class NotebookCellList extends WorkbenchList implements ID } } + revealElementInViewAtTop(cell: ICellViewModel) { + const index = this._getViewIndexUpperBound(cell); + + if (index >= 0) { + this._revealInternal(index, false, CellRevealPosition.Top); + } + } + revealElementInCenterIfOutsideViewport(cell: ICellViewModel) { const index = this._getViewIndexUpperBound(cell); diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts index 41a457e3..39da33ff 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IProcessedOutput, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICellOutputViewModel, ICommonNotebookEditor, IOutputTransformContribution, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { URI } from 'vs/base/common/uri'; export class OutputRenderer { @@ -15,7 +14,7 @@ export class OutputRenderer { protected readonly _mimeTypeMapping: { [key: number]: IOutputTransformContribution; }; constructor( - notebookEditor: INotebookEditor, + notebookEditor: ICommonNotebookEditor, private readonly instantiationService: IInstantiationService ) { this._contributions = {}; @@ -34,7 +33,8 @@ export class OutputRenderer { } } - renderNoop(output: IProcessedOutput, container: HTMLElement): IRenderOutput { + renderNoop(viewModel: ICellOutputViewModel, container: HTMLElement): IRenderOutput { + const output = viewModel.model; const contentNode = document.createElement('p'); contentNode.innerText = `No renderer could be found for output. It has the following output type: ${output.outputKind}`; @@ -42,13 +42,14 @@ export class OutputRenderer { return { type: RenderOutputType.None, hasDynamicHeight: false }; } - render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI | undefined): IRenderOutput { + render(viewModel: ICellOutputViewModel, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI | undefined): IRenderOutput { + const output = viewModel.model; const transform = this._mimeTypeMapping[output.outputKind]; if (transform) { - return transform.render(output, container, preferredMimeType, notebookUri); + return transform.render(viewModel, container, preferredMimeType, notebookUri); } else { - return this.renderNoop(output, container); + return this.renderNoop(viewModel, container); } } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts index 10a4839b..8a0e7216 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts @@ -3,21 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IRenderOutput, CellOutputKind, IErrorOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; import { RGBA, Color } from 'vs/base/common/color'; import { ansiColorIdentifiers } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICommonNotebookEditor, IErrorOutputViewModel, IOutputTransformContribution, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; class ErrorTransform implements IOutputTransformContribution { constructor( - public editor: INotebookEditor, + public editor: ICommonNotebookEditor, @IThemeService private readonly themeService: IThemeService ) { } - render(output: IErrorOutput, container: HTMLElement): IRenderOutput { + render(viewModel: IErrorOutputViewModel, container: HTMLElement): IRenderOutput { + const output = viewModel.model; const header = document.createElement('div'); const headerMessage = output.ename && output.evalue ? `${output.ename}: ${output.evalue}` diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts index b2cec399..420f309b 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IRenderOutput, CellOutputKind, ITransformedDisplayOutputDto, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; import * as DOM from 'vs/base/browser/dom'; -import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICommonNotebookEditor, IDisplayOutputViewModel, IOutputTransformContribution, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { isArray } from 'vs/base/common/types'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; @@ -22,10 +22,10 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; class RichRenderer implements IOutputTransformContribution { - private _richMimeTypeRenderers = new Map IRenderOutput>(); + private _richMimeTypeRenderers = new Map IRenderOutput>(); constructor( - public notebookEditor: INotebookEditor, + public notebookEditor: ICommonNotebookEditor, @IInstantiationService private readonly instantiationService: IInstantiationService, @IModelService private readonly modelService: IModelService, @IModeService private readonly modeService: IModeService, @@ -44,8 +44,8 @@ class RichRenderer implements IOutputTransformContribution { this._richMimeTypeRenderers.set('text/x-javascript', this.renderCode.bind(this)); } - render(output: ITransformedDisplayOutputDto, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI): IRenderOutput { - if (!output.data) { + render(output: IDisplayOutputViewModel, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI): IRenderOutput { + if (!output.model.data) { const contentNode = document.createElement('p'); contentNode.innerText = `No data could be found for output.`; container.appendChild(contentNode); @@ -55,7 +55,7 @@ class RichRenderer implements IOutputTransformContribution { if (!preferredMimeType || !this._richMimeTypeRenderers.has(preferredMimeType)) { const contentNode = document.createElement('p'); const mimeTypes = []; - for (const property in output.data) { + for (const property in output.model.data) { mimeTypes.push(property); } @@ -75,8 +75,8 @@ class RichRenderer implements IOutputTransformContribution { return renderer!(output, notebookUri, container); } - renderJSON(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { - const data = output.data['application/json']; + renderJSON(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { + const data = output.model.data['application/json']; const str = JSON.stringify(data, null, '\t'); const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { @@ -94,8 +94,8 @@ class RichRenderer implements IOutputTransformContribution { const textModel = this.modelService.createModel(str, mode, resource, false); editor.setModel(textModel); - const width = this.notebookEditor.getLayoutInfo().width; - const fontInfo = this.notebookEditor.getLayoutInfo().fontInfo; + const width = this.notebookEditor.getCellOutputLayoutInfo(output.cellViewModel).width; + const fontInfo = this.notebookEditor.getCellOutputLayoutInfo(output.cellViewModel).fontInfo; const height = Math.min(textModel.getLineCount(), 16) * (fontInfo.lineHeight || 18); editor.layout({ @@ -108,8 +108,8 @@ class RichRenderer implements IOutputTransformContribution { return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderCode(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { - const data = output.data['text/x-javascript']; + renderCode(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { + const data = output.model.data['text/x-javascript']; const str = (isArray(data) ? data.join('') : data) as string; const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { @@ -127,8 +127,8 @@ class RichRenderer implements IOutputTransformContribution { const textModel = this.modelService.createModel(str, mode, resource, false); editor.setModel(textModel); - const width = this.notebookEditor.getLayoutInfo().width; - const fontInfo = this.notebookEditor.getLayoutInfo().fontInfo; + const width = this.notebookEditor.getCellOutputLayoutInfo(output.cellViewModel).width; + const fontInfo = this.notebookEditor.getCellOutputLayoutInfo(output.cellViewModel).fontInfo; const height = Math.min(textModel.getLineCount(), 16) * (fontInfo.lineHeight || 18); editor.layout({ @@ -141,8 +141,8 @@ class RichRenderer implements IOutputTransformContribution { return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderJavaScript(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { - const data = output.data['application/javascript']; + renderJavaScript(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { + const data = output.model.data['application/javascript']; const str = isArray(data) ? data.join('') : data; const scriptVal = ``; return { @@ -153,8 +153,8 @@ class RichRenderer implements IOutputTransformContribution { }; } - renderHTML(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { - const data = output.data['text/html']; + renderHTML(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { + const data = output.model.data['text/html']; const str = (isArray(data) ? data.join('') : data) as string; return { type: RenderOutputType.Html, @@ -164,8 +164,8 @@ class RichRenderer implements IOutputTransformContribution { }; } - renderSVG(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { - const data = output.data['image/svg+xml']; + renderSVG(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { + const data = output.model.data['image/svg+xml']; const str = (isArray(data) ? data.join('') : data) as string; return { type: RenderOutputType.Html, @@ -175,8 +175,8 @@ class RichRenderer implements IOutputTransformContribution { }; } - renderMarkdown(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { - const data = output.data['text/markdown']; + renderMarkdown(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { + const data = output.model.data['text/markdown']; const str = (isArray(data) ? data.join('') : data) as string; const mdOutput = document.createElement('div'); const mdRenderer = this.instantiationService.createInstance(MarkdownRenderer, { baseUrl: dirname(notebookUri) }); @@ -186,9 +186,9 @@ class RichRenderer implements IOutputTransformContribution { return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderPNG(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { + renderPNG(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { const image = document.createElement('img'); - image.src = `data:image/png;base64,${output.data['image/png']}`; + image.src = `data:image/png;base64,${output.model.data['image/png']}`; const display = document.createElement('div'); display.classList.add('display'); display.appendChild(image); @@ -196,9 +196,9 @@ class RichRenderer implements IOutputTransformContribution { return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderJPEG(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { + renderJPEG(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { const image = document.createElement('img'); - image.src = `data:image/jpeg;base64,${output.data['image/jpeg']}`; + image.src = `data:image/jpeg;base64,${output.model.data['image/jpeg']}`; const display = document.createElement('div'); display.classList.add('display'); display.appendChild(image); @@ -206,8 +206,8 @@ class RichRenderer implements IOutputTransformContribution { return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderPlainText(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { - const data = output.data['text/plain']; + renderPlainText(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { + const data = output.model.data['text/plain']; const contentNode = DOM.$('.output-plaintext'); truncatedArrayOfString(contentNode, isArray(data) ? data : [data], this.openerService, this.textFileService, this.themeService, true); container.appendChild(contentNode); diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts index b05a25d3..5f6f9b77 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { IRenderOutput, CellOutputKind, IStreamOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; -import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICommonNotebookEditor, IOutputTransformContribution, IRenderOutput, IStreamOutputViewModel, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { truncatedArrayOfString } from 'vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; @@ -14,14 +14,15 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; class StreamRenderer implements IOutputTransformContribution { constructor( - editor: INotebookEditor, + editor: ICommonNotebookEditor, @IOpenerService readonly openerService: IOpenerService, @ITextFileService readonly textFileService: ITextFileService, @IThemeService readonly themeService: IThemeService ) { } - render(output: IStreamOutput, container: HTMLElement): IRenderOutput { + render(viewModel: IStreamOutputViewModel, container: HTMLElement): IRenderOutput { + const output = viewModel.model; const contentNode = DOM.$('.output-stream'); truncatedArrayOfString(contentNode, [output.text], this.openerService, this.textFileService, this.themeService, false); container.appendChild(contentNode); diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper.ts index 8ef36f85..c8dc7731 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper.ts @@ -61,14 +61,14 @@ export function truncatedArrayOfString(container: HTMLElement, outputs: string[] const bufferBuilder = new PieceTreeTextBufferBuilder(); outputs.forEach(output => bufferBuilder.acceptChunk(output)); const factory = bufferBuilder.finish(); - buffer = factory.create(DefaultEndOfLine.LF); + buffer = factory.create(DefaultEndOfLine.LF).textBuffer; const sizeBufferLimitPosition = buffer.getPositionAt(SIZE_LIMIT); if (sizeBufferLimitPosition.lineNumber < LINES_LIMIT) { const truncatedText = buffer.getValueInRange(new Range(1, 1, sizeBufferLimitPosition.lineNumber, sizeBufferLimitPosition.column), EndOfLinePreference.TextDefined); if (renderANSI) { container.appendChild(handleANSIOutput(truncatedText, themeService)); } else { - const pre = DOM.$('div'); + const pre = DOM.$('pre'); pre.innerText = truncatedText; container.appendChild(pre); } @@ -83,21 +83,32 @@ export function truncatedArrayOfString(container: HTMLElement, outputs: string[] const bufferBuilder = new PieceTreeTextBufferBuilder(); outputs.forEach(output => bufferBuilder.acceptChunk(output)); const factory = bufferBuilder.finish(); - buffer = factory.create(DefaultEndOfLine.LF); + buffer = factory.create(DefaultEndOfLine.LF).textBuffer; } if (buffer.getLineCount() < LINES_LIMIT) { const lineCount = buffer.getLineCount(); - const fullRange = new Range(1, 1, lineCount, buffer.getLineLastNonWhitespaceColumn(lineCount)); + const fullRange = new Range(1, 1, lineCount, Math.max(1, buffer.getLineLastNonWhitespaceColumn(lineCount))); - container.innerText = buffer.getValueInRange(fullRange, EndOfLinePreference.TextDefined); + if (renderANSI) { + const pre = DOM.$('pre'); + container.appendChild(pre); + + pre.appendChild(handleANSIOutput(buffer.getValueInRange(fullRange, EndOfLinePreference.TextDefined), themeService)); + } else { + const pre = DOM.$('pre'); + container.appendChild(pre); + pre.innerText = buffer.getValueInRange(fullRange, EndOfLinePreference.TextDefined); + } return; } if (renderANSI) { - container.appendChild(handleANSIOutput(buffer.getValueInRange(new Range(1, 1, LINES_LIMIT - 5, buffer.getLineLastNonWhitespaceColumn(LINES_LIMIT - 5)), EndOfLinePreference.TextDefined), themeService)); + const pre = DOM.$('pre'); + container.appendChild(pre); + pre.appendChild(handleANSIOutput(buffer.getValueInRange(new Range(1, 1, LINES_LIMIT - 5, buffer.getLineLastNonWhitespaceColumn(LINES_LIMIT - 5)), EndOfLinePreference.TextDefined), themeService)); } else { - const pre = DOM.$('div'); + const pre = DOM.$('pre'); pre.innerText = buffer.getValueInRange(new Range(1, 1, LINES_LIMIT - 5, buffer.getLineLastNonWhitespaceColumn(LINES_LIMIT - 5)), EndOfLinePreference.TextDefined); container.appendChild(pre); } @@ -108,7 +119,9 @@ export function truncatedArrayOfString(container: HTMLElement, outputs: string[] const lineCount = buffer.getLineCount(); if (renderANSI) { - container.appendChild(handleANSIOutput(buffer.getValueInRange(new Range(lineCount - 5, 1, lineCount, buffer.getLineLastNonWhitespaceColumn(lineCount)), EndOfLinePreference.TextDefined), themeService)); + const pre = DOM.$('div'); + container.appendChild(pre); + pre.appendChild(handleANSIOutput(buffer.getValueInRange(new Range(lineCount - 5, 1, lineCount, buffer.getLineLastNonWhitespaceColumn(lineCount)), EndOfLinePreference.TextDefined), themeService)); } else { const post = DOM.$('div'); post.innerText = buffer.getValueInRange(new Range(lineCount - 5, 1, lineCount, buffer.getLineLastNonWhitespaceColumn(lineCount)), EndOfLinePreference.TextDefined); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 31fc6590..ce56dffa 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -10,14 +10,11 @@ import { isWeb } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import * as UUID from 'vs/base/common/uuid'; import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; -import { CELL_MARGIN, CELL_RUN_GUTTER, CODE_CELL_LEFT_MARGIN, CELL_OUTPUT_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; -import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; -import { CellOutputKind, IDisplayOutput, IInsetRenderOutput, INotebookRendererInfo, IProcessedOutput, ITransformedDisplayOutputDto, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICommonCellInfo, ICommonNotebookEditor, IDisplayOutputLayoutUpdateRequest, IDisplayOutputViewModel, IGenericCellViewModel, IInsetRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellOutputKind, IDisplayOutput, INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IWebviewService, WebviewElement, WebviewContentPurpose } from 'vs/workbench/contrib/webview/browser/webview'; import { asWebviewUri } from 'vs/workbench/contrib/webview/common/webviewUri'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { dirname, joinPath } from 'vs/base/common/resources'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { preloadsScriptStr } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; @@ -26,6 +23,7 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IFileService } from 'vs/platform/files/common/files'; import { VSBuffer } from 'vs/base/common/buffer'; import { getExtensionForMimeType } from 'vs/base/common/mime'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export interface WebviewIntialized { __vscode_notebook_message: boolean; @@ -36,6 +34,7 @@ export interface IDimensionMessage { __vscode_notebook_message: boolean; type: 'dimension'; id: string; + init: boolean; data: DOM.Dimension; } @@ -196,9 +195,9 @@ export type ToWebviewMessage = export type AnyMessage = FromWebviewMessage | ToWebviewMessage; -interface ICachedInset { +export interface ICachedInset { outputId: string; - cell: CodeCellViewModel; + cellInfo: K; renderer?: INotebookRendererInfo; cachedCreation: ICreationRequestMessage; } @@ -217,12 +216,12 @@ export interface INotebookWebviewMessage { } let version = 0; -export class BackLayerWebView extends Disposable { +export class BackLayerWebView extends Disposable { element: HTMLElement; webview: WebviewElement | undefined = undefined; - insetMapping: Map = new Map(); - hiddenInsetMapping: Set = new Set(); - reversedInsetMapping: Map = new Map(); + insetMapping: Map> = new Map(); + hiddenInsetMapping: Set = new Set(); + reversedInsetMapping: Map = new Map(); localResourceRootsCache: URI[] | undefined = undefined; rendererRootsCache: URI[] = []; kernelRootsCache: URI[] = []; @@ -234,9 +233,13 @@ export class BackLayerWebView extends Disposable { private _disposed = false; constructor( - public notebookEditor: INotebookEditor, + public notebookEditor: ICommonNotebookEditor, public id: string, public documentUri: URI, + public options: { + outputNodePadding: number, + outputNodeLeftPadding: number + }, @IWebviewService readonly webviewService: IWebviewService, @IOpenerService readonly openerService: IOpenerService, @INotebookService private readonly notebookService: INotebookService, @@ -249,12 +252,10 @@ export class BackLayerWebView extends Disposable { this.element = document.createElement('div'); - this.element.style.width = `calc(100% - ${CODE_CELL_LEFT_MARGIN + (CELL_MARGIN * 2) + CELL_RUN_GUTTER}px)`; this.element.style.height = '1400px'; this.element.style.position = 'absolute'; - this.element.style.margin = `0px 0 0px ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px`; } - generateContent(outputNodePadding: number, coreDependencies: string, baseUrl: string) { + generateContent(coreDependencies: string, baseUrl: string) { return html` @@ -263,7 +264,7 @@ export class BackLayerWebView extends Disposable {