diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 827166823d7..bd2425200ec 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -58,7 +58,7 @@ You may see improved VNC responsiveness when accessing a codespace from VS Code 2. After the VS Code is up and running, press Ctrl/Cmd + Shift + P or F1, choose **Codespaces: Create New Codespace**, and use the following settings: - `microsoft/vscode` for the repository. - - Select any branch (e.g. **main**) - you select a different one later. + - Select any branch (e.g. **main**) - you can select a different one later. - Choose **Standard** (4-core, 8GB) as the size. 4. After you have connected to the codespace, you can use a [VNC Viewer](https://www.realvnc.com/en/connect/download/viewer/) to connect to `localhost:5901` and enter `vscode` as the password. diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6623033a491..b9a287013f8 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,7 @@ // Image contents: https://github.com/microsoft/vscode-dev-containers/blob/master/repository-containers/images/github.com/microsoft/vscode/.devcontainer/base.Dockerfile "image": "mcr.microsoft.com/vscode/devcontainers/repos/microsoft/vscode:branch-main", "overrideCommand": false, - "runArgs": [ "--init", "--security-opt", "seccomp=unconfined"], + "runArgs": [ "--init", "--security-opt", "seccomp=unconfined", "--shm-size=1g"], "settings": { "resmon.show.battery": false, diff --git a/.eslintrc.json b/.eslintrc.json index ffc218dfa94..e55fa836ac9 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -133,7 +133,9 @@ "restrictions": [ "vs/nls", "**/vs/base/{common,node}/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { @@ -171,7 +173,9 @@ "vs/nls", "**/vs/base/{common,node}/**", "**/vs/base/parts/*/{common,node}/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { @@ -190,7 +194,9 @@ "vs/css!./**/*", "**/vs/base/{common,browser,node,electron-sandbox,electron-browser}/**", "**/vs/base/parts/*/{common,browser,node,electron-sandbox,electron-browser}/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { @@ -199,7 +205,9 @@ "vs/nls", "**/vs/base/{common,node,electron-main}/**", "**/vs/base/parts/*/{common,node,electron-main}/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { @@ -208,7 +216,8 @@ "vs/nls", "**/vs/base/common/**", "**/vs/base/parts/*/common/**", - "**/vs/platform/*/common/**" + "**/vs/platform/*/common/**", + "tas-client-umd" ] }, { @@ -256,7 +265,9 @@ "**/vs/base/{common,node}/**", "**/vs/base/parts/*/{common,node}/**", "**/vs/platform/*/{common,node}/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { @@ -277,7 +288,9 @@ "**/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}/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { @@ -287,7 +300,9 @@ "**/vs/base/{common,node,electron-main}/**", "**/vs/base/parts/*/{common,node,electron-main}/**", "**/vs/platform/*/{common,node,electron-main}/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { @@ -514,7 +529,9 @@ "**/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}/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { @@ -529,7 +546,9 @@ "vs/workbench/contrib/files/browser/editors/fileEditorInput", "**/vs/workbench/services/**", "**/vs/workbench/test/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { @@ -593,7 +612,9 @@ "**/vs/workbench/{common,node}/**", "**/vs/workbench/api/{common,node}/**", "**/vs/workbench/services/**/{common,node}/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { @@ -624,7 +645,9 @@ "**/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}/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { @@ -743,7 +766,9 @@ "**/vs/workbench/api/{common,node}/**", "**/vs/workbench/services/**/{common,node}/**", "**/vs/workbench/contrib/**/{common,node}/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { @@ -776,7 +801,9 @@ "**/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}/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { @@ -799,7 +826,9 @@ "**/vs/base/parts/**/{common,node}/**", "**/vs/platform/**/{common,node}/**", "**/vs/code/**/{common,node}/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { @@ -811,7 +840,9 @@ "**/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}/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { @@ -822,7 +853,9 @@ "**/vs/base/parts/**/{common,node,electron-main}/**", "**/vs/platform/**/{common,node,electron-main}/**", "**/vs/code/**/{common,node,electron-main}/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { @@ -834,7 +867,9 @@ "**/vs/platform/**/{common,node}/**", "**/vs/workbench/**/{common,node}/**", "**/vs/server/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { @@ -905,28 +940,36 @@ "target": "**/test/smoke/**", "restrictions": [ "**/test/smoke/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { "target": "**/test/automation/**", "restrictions": [ "**/test/automation/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { "target": "**/test/integration/**", "restrictions": [ "**/test/integration/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { "target": "**/test/monaco/**", "restrictions": [ "**/test/monaco/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { @@ -944,21 +987,27 @@ "target": "**/{node,electron-browser,electron-main}/**/*.test.ts", "restrictions": [ "**/vs/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { "target": "**/{node,electron-browser,electron-main}/**/test/**", "restrictions": [ "**/vs/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { "target": "**/test/{node,electron-browser,electron-main}/**", "restrictions": [ "**/vs/**", - "@vscode/*", "@parcel/*", "*" // node modules + "@vscode/*", + "@parcel/*", + "*" // node modules ] }, { @@ -1004,6 +1053,14 @@ "jsdoc/no-types": "off" } }, + { + "files": [ + "**/*.test.ts" + ], + "rules": { + "code-no-test-only": "error" + } + }, { "files": [ "**/vscode.d.ts", diff --git a/.github/classifier.json b/.github/classifier.json index f1e3f973b45..d7b302ac0a0 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -2,7 +2,7 @@ "$schema": "https://raw.githubusercontent.com/microsoft/vscode-github-triage-actions/master/classifier-deep/apply/apply-labels/deep-classifier-config.schema.json", "vacation": ["RMacfarlane", "eamodio"], "assignees": { - "JacksonKearl": {"accuracy": 0.5} + "nameToOverrideAccuracyOf": {"accuracy": 0.8} }, "labels": { "L10N": {"assign": ["TylerLeonhardt", "csigs"]}, @@ -12,6 +12,8 @@ "api-finalization": {"assign": []}, "api-proposal": {"assign": ["jrieken"]}, "authentication": {"assign": ["TylerLeonhardt"]}, + "bracket-pair-colorization": {"assign": ["hediet"]}, + "bracket-pair-guides": {"assign": ["hediet"]}, "breadcrumbs": {"assign": ["jrieken"]}, "callhierarchy": {"assign": ["jrieken"]}, "code-lens": {"assign": ["jrieken"]}, @@ -23,55 +25,59 @@ "custom-editors": {"assign": ["mjbvz"]}, "debug": {"assign": ["weinand"]}, "dialogs": {"assign": ["sbatten"]}, - "diff-editor": {"assign": []}, + "diff-editor": {"assign": ["alexdima"]}, "dropdown": {"assign": []}, - "editor": {"assign": ["rebornix"]}, - "editor-autoclosing": {"assign": []}, + "editor-api": {"assign": ["alexdima"]}, + "editor-autoclosing": {"assign": ["alexdima"]}, "editor-autoindent": {"assign": ["rebornix"]}, - "editor-bracket-matching": {"assign": []}, - "editor-clipboard": {"assign": ["jrieken"]}, + "editor-bracket-matching": {"assign": ["hediet"]}, + "editor-clipboard": {"assign": ["alexdima", "rebornix"]}, "editor-code-actions": {"assign": []}, "editor-color-picker": {"assign": ["rebornix"]}, "editor-columnselect": {"assign": ["alexdima"]}, - "editor-commands": {"assign": ["jrieken"]}, - "editor-comments": {"assign": []}, - "editor-contrib": {"assign": []}, - "editor-core": {"assign": []}, + "editor-commands": {"assign": ["alexdima"]}, + "editor-comments": {"assign": ["alexdima"]}, + "editor-contrib": {"assign": ["alexdima"]}, + "editor-core": {"assign": ["alexdima"]}, "editor-drag-and-drop": {"assign": ["rebornix"]}, "editor-error-widget": {"assign": ["sandy081"]}, "editor-find": {"assign": ["rebornix"]}, "editor-folding": {"assign": ["aeschli"]}, + "editor-highlight": {"assign": ["alexdima"]}, "editor-hover": {"assign": []}, - "editor-indent-guides": {"assign": []}, + "editor-indent-detection": {"assign": ["alexdima"]}, + "editor-indent-guides": {"assign": ["hediet"]}, "editor-input": {"assign": ["alexdima"]}, - "editor-input-IME": {"assign": ["rebornix"]}, - "editor-minimap": {"assign": []}, + "editor-input-IME": {"assign": ["alexdima", "rebornix"]}, + "editor-minimap": {"assign": ["alexdima"]}, "editor-multicursor": {"assign": ["alexdima"]}, "editor-parameter-hints": {"assign": []}, - "editor-render-whitespace": {"assign": []}, + "editor-render-whitespace": {"assign": ["alexdima"]}, "editor-rendering": {"assign": ["alexdima"]}, - "editor-scrollbar": {"assign": []}, + "editor-RTL": {"assign": ["alexdima"]}, + "editor-scrollbar": {"assign": ["alexdima"]}, "editor-symbols": {"assign": ["jrieken"]}, "editor-synced-region": {"assign": ["aeschli"]}, "editor-textbuffer": {"assign": ["rebornix"]}, - "editor-theming": {"assign": []}, + "editor-theming": {"assign": ["alexdima"]}, "editor-wordnav": {"assign": ["alexdima"]}, "editor-wrapping": {"assign": ["alexdima"]}, "emmet": {"assign": ["rzhao271"]}, "error-list": {"assign": ["sandy081"]}, "explorer-custom": {"assign": ["sandy081"]}, - "extension-host": {"assign": []}, + "extension-host": {"assign": ["alexdima"]}, "extensions": {"assign": ["sandy081"]}, "extensions-development": {"assign": []}, "file-decorations": {"assign": ["jrieken"]}, "file-encoding": {"assign": ["bpasero"]}, "file-explorer": {"assign": ["JacksonKearl"]}, - "file-glob": {"assign": []}, + "file-glob": {"assign": ["bpasero"]}, "file-guess-encoding": {"assign": ["bpasero"]}, "file-io": {"assign": ["bpasero"]}, "file-watcher": {"assign": ["bpasero"]}, "font-rendering": {"assign": []}, "formatting": {"assign": []}, + "ghost-text": {"assign": ["hediet"]}, "git": {"assign": ["lszomoru"]}, "gpu": {"assign": ["deepak1556"]}, "grammar": {"assign": ["mjbvz"]}, @@ -80,6 +86,8 @@ "i18n": {"assign": []}, "icon-brand": {"assign": []}, "icons-product": {"assign": ["misolori"]}, + "inlay-hints": {"assign": ["jrieken", "hediet"]}, + "inline-completions": {"assign": ["hediet"]}, "install-update": {"assign": []}, "terminal": {"assign": ["meganrogge"]}, "terminal-conpty": {"assign": ["meganrogge"]}, @@ -92,7 +100,7 @@ "issue-reporter": {"assign": ["TylerLeonhardt"]}, "javascript": {"assign": ["mjbvz"]}, "json": {"assign": ["aeschli"]}, - "keybindings": {"assign": []}, + "keybindings": {"assign": ["alexdima"]}, "keybindings-editor": {"assign": ["sandy081"]}, "keyboard-layout": {"assign": ["alexdima"]}, "languages-basic": {"assign": ["aeschli"]}, @@ -114,10 +122,12 @@ "php": {"assign": ["roblourens"]}, "portable-mode": {"assign": ["joaomoreno"]}, "proxy": {"assign": []}, + "quick-open": {"assign": ["TylerLeonhardt"]}, "quick-pick": {"assign": ["TylerLeonhardt"]}, "references-viewlet": {"assign": ["jrieken"]}, "release-notes": {"assign": []}, "remote": {"assign": []}, + "remote-connection": {"assign": ["alexdima"]}, "remote-explorer": {"assign": ["alexr00"]}, "rename": {"assign": ["jrieken"]}, "scm": {"assign": ["lszomoru"]}, @@ -125,7 +135,7 @@ "search": {"assign": ["roblourens"]}, "search-editor": {"assign": ["JacksonKearl"]}, "search-replace": {"assign": ["sandy081"]}, - "semantic-tokens": {"assign": ["aeschli"]}, + "semantic-tokens": {"assign": ["alexdima", "aeschli"]}, "settings-editor": {"assign": ["rzhao271"]}, "settings-sync": {"assign": ["sandy081"]}, "simple-file-dialog": {"assign": ["alexr00"]}, @@ -142,36 +152,42 @@ "timeline-git": {"assign": ["lszomoru"]}, "titlebar": {"assign": ["sbatten"]}, "tokenization": {"assign": []}, + "trackpad/scroll": {"assign": []}, "tree": {"assign": ["joaomoreno"]}, "typescript": {"assign": ["mjbvz"]}, - "undo-redo": {"assign": []}, + "undo-redo": {"assign": ["alexdima"]}, "unit-test": {"assign": []}, "uri": {"assign": ["jrieken"]}, "ux": {"assign": ["misolori"]}, "variable-resolving": {"assign": []}, "vscode-build": {"assign": []}, - "web": {"assign": ["bpasero"]}, + "web": {"assign": []}, "webview": {"assign": ["mjbvz"]}, - "workbench-cli": {"assign": []}, + "workbench-actions": {"assign": ["bpasero"]}, + "workbench-cli": {"assign": ["bpasero"]}, "workbench-diagnostics": {"assign": ["Tyriar"]}, "workbench-dnd": {"assign": ["bpasero"]}, "workbench-editor-grid": {"assign": ["sbatten"]}, + "workbench-editor-groups": {"assign": ["bpasero"]}, + "workbench-editor-resolver": {"assign": ["lramos15"]}, "workbench-editors": {"assign": ["bpasero"]}, "workbench-electron": {"assign": ["deepak1556"]}, "workbench-feedback": {"assign": ["bpasero"]}, "workbench-history": {"assign": ["bpasero"]}, - "workbench-hot-exit": {"assign": []}, + "workbench-hot-exit": {"assign": ["bpasero"]}, + "workbench-hover": {"assign": ["Tyriar"]}, "workbench-launch": {"assign": []}, "workbench-link": {"assign": []}, "workbench-multiroot": {"assign": ["bpasero"]}, "workbench-notifications": {"assign": ["bpasero"]}, - "workbench-os-integration": {"assign": []}, + "workbench-os-integration": {"assign": ["bpasero"]}, "workbench-rapid-render": {"assign": ["jrieken"]}, - "workbench-run-as-admin": {"assign": []}, + "workbench-run-as-admin": {"assign": ["bpasero"]}, "workbench-state": {"assign": ["bpasero"]}, "workbench-status": {"assign": ["bpasero"]}, "workbench-tabs": {"assign": ["bpasero"]}, "workbench-touchbar": {"assign": ["bpasero"]}, + "workbench-untitled-editors": {"assign": ["bpasero"]}, "workbench-views": {"assign": ["sbatten"]}, "workbench-welcome": {"assign": ["JacksonKearl"]}, "workbench-window": {"assign": ["bpasero"]}, diff --git a/.vscode/notebooks/api.github-issues b/.vscode/notebooks/api.github-issues index 8eb4c01846a..cf580ac17be 100644 --- a/.vscode/notebooks/api.github-issues +++ b/.vscode/notebooks/api.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$repo=repo:microsoft/vscode\n$milestone=milestone:\"October 2021\"" + "value": "$repo=repo:microsoft/vscode\n$milestone=milestone:\"November 2021\"" }, { "kind": 1, diff --git a/.vscode/notebooks/my-work.github-issues b/.vscode/notebooks/my-work.github-issues index 6af557336c8..f207cf5e4b5 100644 --- a/.vscode/notebooks/my-work.github-issues +++ b/.vscode/notebooks/my-work.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "// list of repos we work in\n$repos=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-references-view repo:microsoft/vscode-anycode repo:microsoft/vscode-hexeditor repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-livepreview repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-remote-repositories-github\n\n// current milestone name\n$milestone=milestone:\"October 2021\"" + "value": "// list of repos we work in\n$repos=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-references-view repo:microsoft/vscode-anycode repo:microsoft/vscode-hexeditor repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-livepreview repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-remote-repositories-github\n\n// current milestone name\n$milestone=milestone:\"November 2021\"" }, { "kind": 1, diff --git a/.vscode/notebooks/vscode-dev.github-issues b/.vscode/notebooks/vscode-dev.github-issues new file mode 100644 index 00000000000..42a1b2e7309 --- /dev/null +++ b/.vscode/notebooks/vscode-dev.github-issues @@ -0,0 +1,42 @@ +[ + { + "kind": 1, + "language": "markdown", + "value": "# vscode.dev repo" + }, + { + "kind": 2, + "language": "github-issues", + "value": "repo:microsoft/vscode-dev milestone:\"November 2021\" is:open" + }, + { + "kind": 2, + "language": "github-issues", + "value": "repo:microsoft/vscode-dev milestone:\"Backlog\" is:open" + }, + { + "kind": 1, + "language": "markdown", + "value": "# VS Code repo" + }, + { + "kind": 2, + "language": "github-issues", + "value": "repo:microsoft/vscode label:vscode.dev is:open" + }, + { + "kind": 1, + "language": "markdown", + "value": "# GitHub Repositories repos" + }, + { + "kind": 2, + "language": "github-issues", + "value": "repo:microsoft/vscode-remote-repositories-github milestone:\"November 2021\" is:open" + }, + { + "kind": 2, + "language": "github-issues", + "value": "repo:microsoft/vscode-remotehub milestone:\"November 2021\" is:open" + } +] \ No newline at end of file diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index 22818f0fa27..1334cd15d6e 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -102,10 +102,6 @@ const serverEntryPoints = [ name: 'vs/server/remoteExtensionHostProcess', exclude: ['vs/css', 'vs/nls'] }, - { - name: 'vs/platform/files/node/watcher/unix/watcherApp', - exclude: ['vs/css', 'vs/nls'] - }, { name: 'vs/platform/files/node/watcher/nsfw/watcherApp', exclude: ['vs/css', 'vs/nls'] diff --git a/build/lib/eslint/code-no-test-only.js b/build/lib/eslint/code-no-test-only.js new file mode 100644 index 00000000000..46d144bfcaf --- /dev/null +++ b/build/lib/eslint/code-no-test-only.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. + *--------------------------------------------------------------------------------------------*/ +module.exports = new class NoTestOnly { + create(context) { + return { + ['MemberExpression[object.name="test"][property.name="only"]']: (node) => { + return context.report({ + node, + message: 'test.only is a dev-time tool and CANNOT be pushed' + }); + } + }; + } +}; diff --git a/build/lib/eslint/code-no-test-only.ts b/build/lib/eslint/code-no-test-only.ts new file mode 100644 index 00000000000..a04483ea6ab --- /dev/null +++ b/build/lib/eslint/code-no-test-only.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; + +export = new class NoTestOnly implements eslint.Rule.RuleModule { + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + return { + ['MemberExpression[object.name="test"][property.name="only"]']: (node: any) => { + return context.report({ + node, + message: 'test.only is a dev-time tool and CANNOT be pushed' + }); + } + }; + } +}; diff --git a/build/package.json b/build/package.json index 1d69f4e0a96..e8b09a91a01 100644 --- a/build/package.json +++ b/build/package.json @@ -62,7 +62,7 @@ "plist": "^3.0.1", "source-map": "0.6.1", "tmp": "^0.2.1", - "typescript": "^4.5.0-dev.20211021", + "typescript": "^4.5.0-dev.20211029", "vsce": "^1.100.0", "vscode-universal-bundler": "^0.0.2" }, diff --git a/build/yarn.lock b/build/yarn.lock index d47575a5801..a2dcaaf383d 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -2557,10 +2557,10 @@ typed-rest-client@^1.8.4: tunnel "0.0.6" underscore "^1.12.1" -typescript@^4.5.0-dev.20211021: - version "4.5.0-dev.20211021" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.0-dev.20211021.tgz#7bd786f53c33f7ade525d0b34bda27f2e01927fe" - integrity sha512-lZJTNSL8CBqgURSrVwCto8WEV1U0BUwoy+GTmINoRc/Sm2BcYiRpcq/OAiSg/1q/rvODousdOsHePfAZfwi0Qg== +typescript@^4.5.0-dev.20211029: + version "4.5.0-dev.20211029" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.0-dev.20211029.tgz#ec4619ab136bd70ddd9ec1a7c18783b7ce9990a3" + integrity sha512-N+2wLMbTq+jQmad78i4wKBGcXudBFWy+QdV1Xu9cx+F5Xi6hubBotFEzS7zA7G1Eevy6NJwlsNy0G8ok2GQ9nw== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.5" diff --git a/extensions/cpp/cgmanifest.json b/extensions/cpp/cgmanifest.json index e09a7c4125a..f2bf269a5f4 100644 --- a/extensions/cpp/cgmanifest.json +++ b/extensions/cpp/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "jeff-hykin/cpp-textmate-grammar", "repositoryUrl": "https://github.com/jeff-hykin/cpp-textmate-grammar", - "commitHash": "cb4565d3ac2e04138effb732b1217650636eb1de" + "commitHash": "db3f4e4a5d8335b2f6d689bec490c23f8313630f" } }, "license": "MIT", diff --git a/extensions/cpp/syntaxes/c.tmLanguage.json b/extensions/cpp/syntaxes/c.tmLanguage.json index 7ce64b81de7..80852872cd8 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/f074a48ae0b7ba313af3faf3d8bfda8537864bd1", + "version": "https://github.com/jeff-hykin/cpp-textmate-grammar/commit/0ef79f098ed80ce5a86be4ed40f99ebcdbac4895", "name": "C", "scopeName": "source.c", "patterns": [ diff --git a/extensions/cpp/syntaxes/cpp.embedded.macro.tmLanguage.json b/extensions/cpp/syntaxes/cpp.embedded.macro.tmLanguage.json index 566e163535a..d7e233b3828 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/cb4565d3ac2e04138effb732b1217650636eb1de", + "version": "https://github.com/jeff-hykin/cpp-textmate-grammar/commit/db3f4e4a5d8335b2f6d689bec490c23f8313630f", "name": "C++", "scopeName": "source.cpp.embedded.macro", "patterns": [ diff --git a/extensions/cpp/syntaxes/cpp.tmLanguage.json b/extensions/cpp/syntaxes/cpp.tmLanguage.json index de7714c0130..2564dd58a04 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/cb4565d3ac2e04138effb732b1217650636eb1de", + "version": "https://github.com/jeff-hykin/cpp-textmate-grammar/commit/db3f4e4a5d8335b2f6d689bec490c23f8313630f", "name": "C++", "scopeName": "source.cpp", "patterns": [ diff --git a/extensions/csharp/cgmanifest.json b/extensions/csharp/cgmanifest.json index 87e298088b9..505f53fe37f 100644 --- a/extensions/csharp/cgmanifest.json +++ b/extensions/csharp/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "dotnet/csharp-tmLanguage", "repositoryUrl": "https://github.com/dotnet/csharp-tmLanguage", - "commitHash": "5426265f1be3f8056a984b709fadf56b9ce4c400" + "commitHash": "483d25bfa2b96474b55d2e9e4d1ca2acbd542034" } }, "license": "MIT", diff --git a/extensions/csharp/syntaxes/csharp.tmLanguage.json b/extensions/csharp/syntaxes/csharp.tmLanguage.json index 44de5f9940b..a9bb170e9dd 100644 --- a/extensions/csharp/syntaxes/csharp.tmLanguage.json +++ b/extensions/csharp/syntaxes/csharp.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/dotnet/csharp-tmLanguage/commit/5426265f1be3f8056a984b709fadf56b9ce4c400", + "version": "https://github.com/dotnet/csharp-tmLanguage/commit/483d25bfa2b96474b55d2e9e4d1ca2acbd542034", "name": "C#", "scopeName": "source.cs", "patterns": [ @@ -530,7 +530,7 @@ "name": "keyword.other.namespace.cs" } }, - "end": "(?<=\\})", + "end": "(?<=\\})|(?=;)", "patterns": [ { "include": "#comment" @@ -3075,27 +3075,27 @@ { "include": "#tuple-literal-element" }, + { + "include": "#expression" + }, { "include": "#punctuation-comma" } ] }, "tuple-literal-element": { - "begin": "(?x)\n(?:(@?[_[:alpha:]][_[:alnum:]]*)\\s*(:)\\s*)?\n(?![,)])", + "begin": "(?x)\n(@?[_[:alpha:]][_[:alnum:]]*)\\s*\n(?=:)", "beginCaptures": { - "0": { - "name": "entity.name.variable.tuple-element.cs" - }, "1": { - "name": "punctuation.separator.colon.cs" + "name": "entity.name.variable.tuple-element.cs" } }, - "end": "(?=[,)])", - "patterns": [ - { - "include": "#expression" + "end": "(:)", + "endCaptures": { + "0": { + "name": "punctuation.separator.colon.cs" } - ] + } }, "expression-operators": { "patterns": [ diff --git a/extensions/css/cgmanifest.json b/extensions/css/cgmanifest.json index 81a927a5984..0ff1e899c0a 100644 --- a/extensions/css/cgmanifest.json +++ b/extensions/css/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "atom/language-css", "repositoryUrl": "https://github.com/atom/language-css", - "commitHash": "033087e8caa1b87ca91ee5b211de118b6b3c311d" + "commitHash": "672168274c7b457f3c118788b5171ae888c1bf07" } }, "licenseDetail": [ @@ -44,7 +44,7 @@ ], "license": "GitHub License", "description": "The file syntaxes/css.tmLanguage.json was derived from https://github.com/atom/language-css which was originally converted from the TextMate bundle https://github.com/textmate/css.tmbundle.", - "version": "0.44.6" + "version": "0.45.0" } ], "version": 1 diff --git a/extensions/css/syntaxes/css.tmLanguage.json b/extensions/css/syntaxes/css.tmLanguage.json index 0e3d8b7e322..213bcefe22d 100644 --- a/extensions/css/syntaxes/css.tmLanguage.json +++ b/extensions/css/syntaxes/css.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/atom/language-css/commit/033087e8caa1b87ca91ee5b211de118b6b3c311d", + "version": "https://github.com/atom/language-css/commit/672168274c7b457f3c118788b5171ae888c1bf07", "name": "CSS", "scopeName": "source.css", "patterns": [ @@ -1814,7 +1814,7 @@ ] }, "tag-names": { - "match": "(?xi) (?\\s,.\\#|){\\[]|/\\*|:[^\\s]|$)", + "match": "(?xi) (?\\s,.\\#|){:\\[]|/\\*|$)", "name": "entity.name.tag.css" }, "unicode-range": { diff --git a/extensions/dart/cgmanifest.json b/extensions/dart/cgmanifest.json index 253098bc77f..9a778f2ced2 100644 --- a/extensions/dart/cgmanifest.json +++ b/extensions/dart/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "dart-lang/dart-syntax-highlight", "repositoryUrl": "https://github.com/dart-lang/dart-syntax-highlight", - "commitHash": "0aaacde81aa9a12cfed8ca4ab619be5d9e9ed00a" + "commitHash": "1fa12423de71bcc75f68371ca4debaebdd989c20" } }, "licenseDetail": [ diff --git a/extensions/dart/syntaxes/dart.tmLanguage.json b/extensions/dart/syntaxes/dart.tmLanguage.json index 3ba171339c3..cfff8dc963d 100644 --- a/extensions/dart/syntaxes/dart.tmLanguage.json +++ b/extensions/dart/syntaxes/dart.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/dart-lang/dart-syntax-highlight/commit/0aaacde81aa9a12cfed8ca4ab619be5d9e9ed00a", + "version": "https://github.com/dart-lang/dart-syntax-highlight/commit/1fa12423de71bcc75f68371ca4debaebdd989c20", "name": "Dart", "scopeName": "source.dart", "patterns": [ @@ -109,9 +109,6 @@ "name": "variable.other.source.dart" } } - }, - { - "match": "(\\* .*)$" } ] }, @@ -223,19 +220,76 @@ "match": "(?)", + "include": "#function-identifier" + } + ] + }, + "class-identifier": { + "patterns": [ + { + "match": "(?]|, )+>)?|bool\\b|num\\b|int\\b|double\\b|dynamic\\b|(void)\\b)", + "captures": { + "1": { + "name": "support.class.dart" + }, + "2": { + "patterns": [ + { + "include": "#type-args" + } + ] + }, + "3": { + "name": "storage.type.primitive.dart" + } + } + } + ] + }, + "function-identifier": { + "patterns": [ + { + "match": "([_$]*[a-z][a-zA-Z0-9_$]*)(<(?:[a-zA-Z0-9_$<>]|, )+>)?(\\(|\\s+=>)", "captures": { "1": { "name": "entity.name.function.dart" + }, + "2": { + "patterns": [ + { + "include": "#type-args" + } + ] } } } ] }, + "type-args": { + "begin": "(<)", + "end": "(>)", + "beginCaptures": { + "1": { + "name": "other.source.dart" + } + }, + "endCaptures": { + "1": { + "name": "other.source.dart" + } + }, + "patterns": [ + { + "include": "#class-identifier" + }, + { + "match": "\\s*,\\s*" + } + ] + }, "keywords": { "patterns": [ { diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 0814bf66ae6..9236438fb54 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1936,7 +1936,7 @@ export class Repository implements Disposable { actionButton = { command: rebaseWhenSync ? 'git.syncRebase' : 'git.sync', - title: localize('scm button sync title', ' Sync Changes $(sync){0}{1}', HEAD.behind ? `${HEAD.behind}$(arrow-down) ` : '', `${HEAD.ahead}$(arrow-up)`), + title: localize('scm button sync title', '$(sync) Sync Changes {0}{1}', HEAD.behind ? `${HEAD.behind}$(arrow-down) ` : '', `${HEAD.ahead}$(arrow-up)`), tooltip: this.syncTooltip, arguments: [this._sourceControl], }; diff --git a/extensions/javascript/javascript-language-configuration.json b/extensions/javascript/javascript-language-configuration.json index 379352c5ac9..d43cc356cb4 100644 --- a/extensions/javascript/javascript-language-configuration.json +++ b/extensions/javascript/javascript-language-configuration.json @@ -24,7 +24,8 @@ ["(", ")"], ["'", "'"], ["\"", "\""], - ["`", "`"] + ["`", "`"], + ["<", ">"] ], "autoCloseBefore": ";:.,=}])>` \n\t", "folding": { diff --git a/extensions/javascript/package.json b/extensions/javascript/package.json index f3f8a537dd0..e3a087bfc92 100644 --- a/extensions/javascript/package.json +++ b/extensions/javascript/package.json @@ -68,6 +68,8 @@ }, "tokenTypes": { "meta.template.expression": "other", + "meta.template.expression string": "string", + "meta.template.expression comment": "comment", "entity.name.type.instance.jsdoc": "other", "entity.name.function.tagged-template": "other", "meta.import string.quoted": "other", @@ -86,6 +88,8 @@ }, "tokenTypes": { "meta.template.expression": "other", + "meta.template.expression string": "string", + "meta.template.expression comment": "comment", "entity.name.type.instance.jsdoc": "other", "entity.name.function.tagged-template": "other", "meta.import string.quoted": "other", diff --git a/extensions/javascript/syntaxes/JavaScript.tmLanguage.json b/extensions/javascript/syntaxes/JavaScript.tmLanguage.json index bdfcee768de..e25adfe34fe 100644 --- a/extensions/javascript/syntaxes/JavaScript.tmLanguage.json +++ b/extensions/javascript/syntaxes/JavaScript.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/microsoft/TypeScript-TmLanguage/commit/33d8371c344f0b54746586313a939f742f9bcd3a", + "version": "https://github.com/microsoft/TypeScript-TmLanguage/commit/644389aef914fc6fbc97a4dd799cc2d1431ffc87", "name": "JavaScript (with React support)", "scopeName": "source.js", "patterns": [ @@ -2302,24 +2302,27 @@ "include": "#comment" }, { - "match": "(?) ?" }, "fenced_code_block_css": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(css|css.erb)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(css|css.erb)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -96,7 +96,7 @@ ] }, "fenced_code_block_basic": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(html|htm|shtml|xhtml|inc|tmpl|tpl)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(html|htm|shtml|xhtml|inc|tmpl|tpl)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -129,7 +129,7 @@ ] }, "fenced_code_block_ini": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(ini|conf)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(ini|conf)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -162,7 +162,7 @@ ] }, "fenced_code_block_java": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(java|bsh)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(java|bsh)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -195,7 +195,7 @@ ] }, "fenced_code_block_lua": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(lua)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(lua)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -228,7 +228,7 @@ ] }, "fenced_code_block_makefile": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(Makefile|makefile|GNUmakefile|OCamlMakefile)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(Makefile|makefile|GNUmakefile|OCamlMakefile)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -261,7 +261,7 @@ ] }, "fenced_code_block_perl": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(perl|pl|pm|pod|t|PL|psgi|vcl)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(perl|pl|pm|pod|t|PL|psgi|vcl)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -294,7 +294,7 @@ ] }, "fenced_code_block_r": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(R|r|s|S|Rprofile|\\{\\.r.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(R|r|s|S|Rprofile|\\{\\.r.+?\\})((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -327,7 +327,7 @@ ] }, "fenced_code_block_ruby": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(ruby|rb|rbx|rjs|Rakefile|rake|cgi|fcgi|gemspec|irbrc|Capfile|ru|prawn|Cheffile|Gemfile|Guardfile|Hobofile|Vagrantfile|Appraisals|Rantfile|Berksfile|Berksfile.lock|Thorfile|Puppetfile)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(ruby|rb|rbx|rjs|Rakefile|rake|cgi|fcgi|gemspec|irbrc|Capfile|ru|prawn|Cheffile|Gemfile|Guardfile|Hobofile|Vagrantfile|Appraisals|Rantfile|Berksfile|Berksfile.lock|Thorfile|Puppetfile)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -360,7 +360,7 @@ ] }, "fenced_code_block_php": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(php|php3|php4|php5|phpt|phtml|aw|ctp)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(php|php3|php4|php5|phpt|phtml|aw|ctp)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -396,7 +396,7 @@ ] }, "fenced_code_block_sql": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(sql|ddl|dml)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(sql|ddl|dml)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -429,7 +429,7 @@ ] }, "fenced_code_block_vs_net": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(vb)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(vb)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -462,7 +462,7 @@ ] }, "fenced_code_block_xml": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(xml|xsd|tld|jsp|pt|cpt|dtml|rss|opml)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(xml|xsd|tld|jsp|pt|cpt|dtml|rss|opml)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -495,7 +495,7 @@ ] }, "fenced_code_block_xsl": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(xsl|xslt)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(xsl|xslt)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -528,7 +528,7 @@ ] }, "fenced_code_block_yaml": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(yaml|yml)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(yaml|yml)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -561,7 +561,7 @@ ] }, "fenced_code_block_dosbatch": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(bat|batch)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(bat|batch)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -594,7 +594,7 @@ ] }, "fenced_code_block_clojure": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(clj|cljs|clojure)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(clj|cljs|clojure)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -627,7 +627,7 @@ ] }, "fenced_code_block_coffee": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(coffee|Cakefile|coffee.erb)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(coffee|Cakefile|coffee.erb)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -660,7 +660,7 @@ ] }, "fenced_code_block_c": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(c|h)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(c|h)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -693,7 +693,7 @@ ] }, "fenced_code_block_cpp": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(cpp|c\\+\\+|cxx)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(cpp|c\\+\\+|cxx)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -726,7 +726,7 @@ ] }, "fenced_code_block_diff": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(patch|diff|rej)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(patch|diff|rej)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -759,7 +759,7 @@ ] }, "fenced_code_block_dockerfile": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(dockerfile|Dockerfile)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(dockerfile|Dockerfile)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -792,7 +792,7 @@ ] }, "fenced_code_block_git_commit": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(COMMIT_EDITMSG|MERGE_MSG)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(COMMIT_EDITMSG|MERGE_MSG)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -825,7 +825,7 @@ ] }, "fenced_code_block_git_rebase": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(git-rebase-todo)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(git-rebase-todo)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -858,7 +858,7 @@ ] }, "fenced_code_block_go": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(go|golang)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(go|golang)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -891,7 +891,7 @@ ] }, "fenced_code_block_groovy": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(groovy|gvy)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(groovy|gvy)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -924,7 +924,7 @@ ] }, "fenced_code_block_pug": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(jade|pug)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(jade|pug)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -957,7 +957,7 @@ ] }, "fenced_code_block_js": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(js|jsx|javascript|es6|mjs|cjs|\\{\\.js.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(js|jsx|javascript|es6|mjs|cjs|\\{\\.js.+?\\})((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -990,7 +990,7 @@ ] }, "fenced_code_block_js_regexp": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(regexp)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(regexp)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1023,7 +1023,7 @@ ] }, "fenced_code_block_json": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(json|json5|sublime-settings|sublime-menu|sublime-keymap|sublime-mousemap|sublime-theme|sublime-build|sublime-project|sublime-completions)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(json|json5|sublime-settings|sublime-menu|sublime-keymap|sublime-mousemap|sublime-theme|sublime-build|sublime-project|sublime-completions)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1056,7 +1056,7 @@ ] }, "fenced_code_block_jsonc": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(jsonc)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(jsonc)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1089,7 +1089,7 @@ ] }, "fenced_code_block_less": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(less)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(less)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1122,7 +1122,7 @@ ] }, "fenced_code_block_objc": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(objectivec|objective-c|mm|objc|obj-c|m|h)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(objectivec|objective-c|mm|objc|obj-c|m|h)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1155,7 +1155,7 @@ ] }, "fenced_code_block_swift": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(swift)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(swift)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1188,7 +1188,7 @@ ] }, "fenced_code_block_scss": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(scss)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(scss)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1221,7 +1221,7 @@ ] }, "fenced_code_block_perl6": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(perl6|p6|pl6|pm6|nqp)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(perl6|p6|pl6|pm6|nqp)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1254,7 +1254,7 @@ ] }, "fenced_code_block_powershell": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(powershell|ps1|psm1|psd1)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(powershell|ps1|psm1|psd1)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1287,7 +1287,7 @@ ] }, "fenced_code_block_python": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(python|py|py3|rpy|pyw|cpy|SConstruct|Sconstruct|sconstruct|SConscript|gyp|gypi|\\{\\.python.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(python|py|py3|rpy|pyw|cpy|SConstruct|Sconstruct|sconstruct|SConscript|gyp|gypi|\\{\\.python.+?\\})((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1320,7 +1320,7 @@ ] }, "fenced_code_block_regexp_python": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(re)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(re)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1353,7 +1353,7 @@ ] }, "fenced_code_block_rust": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(rust|rs|\\{\\.rust.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(rust|rs|\\{\\.rust.+?\\})((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1386,7 +1386,7 @@ ] }, "fenced_code_block_scala": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(scala|sbt)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(scala|sbt)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1419,7 +1419,7 @@ ] }, "fenced_code_block_shell": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(shell|sh|bash|zsh|bashrc|bash_profile|bash_login|profile|bash_logout|.textmate_init|\\{\\.bash.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(shell|sh|bash|zsh|bashrc|bash_profile|bash_login|profile|bash_logout|.textmate_init|\\{\\.bash.+?\\})((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1452,7 +1452,7 @@ ] }, "fenced_code_block_ts": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(typescript|ts)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(typescript|ts)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1485,7 +1485,7 @@ ] }, "fenced_code_block_tsx": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(tsx)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(tsx)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1518,7 +1518,7 @@ ] }, "fenced_code_block_csharp": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(cs|csharp|c#)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(cs|csharp|c#)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1551,7 +1551,7 @@ ] }, "fenced_code_block_fsharp": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(fs|fsharp|f#)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(fs|fsharp|f#)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1584,7 +1584,7 @@ ] }, "fenced_code_block_dart": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(dart)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(dart)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1617,7 +1617,7 @@ ] }, "fenced_code_block_handlebars": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(handlebars|hbs)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(handlebars|hbs)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1650,7 +1650,7 @@ ] }, "fenced_code_block_markdown": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(markdown|md)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(markdown|md)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1683,7 +1683,7 @@ ] }, "fenced_code_block_log": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(log)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(log)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1716,7 +1716,7 @@ ] }, "fenced_code_block_erlang": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(erlang)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(erlang)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1749,7 +1749,7 @@ ] }, "fenced_code_block_elixir": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(elixir)((\\s+|:|\\{|\\?)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(elixir)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -2222,34 +2222,37 @@ "name": "punctuation.definition.link.markdown" }, "8": { - "name": "string.other.link.description.title.markdown" + "name": "markup.underline.link.markdown" }, "9": { - "name": "punctuation.definition.string.begin.markdown" + "name": "string.other.link.description.title.markdown" }, "10": { - "name": "punctuation.definition.string.end.markdown" + "name": "punctuation.definition.string.begin.markdown" }, "11": { - "name": "string.other.link.description.title.markdown" - }, - "12": { - "name": "punctuation.definition.string.begin.markdown" - }, - "13": { "name": "punctuation.definition.string.end.markdown" }, - "14": { + "12": { "name": "string.other.link.description.title.markdown" }, - "15": { + "13": { "name": "punctuation.definition.string.begin.markdown" }, + "14": { + "name": "punctuation.definition.string.end.markdown" + }, + "15": { + "name": "string.other.link.description.title.markdown" + }, "16": { + "name": "punctuation.definition.string.begin.markdown" + }, + "17": { "name": "punctuation.definition.string.end.markdown" } }, - "match": "(?x)\n \\s* # Leading whitespace\n (\\[)([^]]+?)(\\])(:) # Reference name\n [ \\t]* # Optional whitespace\n (?) # The url\n [ \\t]* # Optional whitespace\n (?:\n ((\\().+?(\\))) # Match title in parens…\n | ((\").+?(\")) # or in double quotes…\n | ((').+?(')) # or in single quotes.\n )? # Title is optional\n \\s* # Optional whitespace\n $\n", + "match": "(?x)\n \\s* # Leading whitespace\n (\\[)([^]]+?)(\\])(:) # Reference name\n [ \\t]* # Optional whitespace\n (?:(<)([^\\>]+?)(>)|(\\S+?)) # The url\n [ \\t]* # Optional whitespace\n (?:\n ((\\().+?(\\))) # Match title in parens…\n | ((\").+?(\")) # or in double quotes…\n | ((').+?(')) # or in single quotes.\n )? # Title is optional\n \\s* # Optional whitespace\n $\n", "name": "meta.link.reference.def.markdown" }, "list_paragraph": { @@ -2550,7 +2553,7 @@ "name": "meta.image.reference.markdown" }, "italic": { - "begin": "(?x) (?(\\*(?=\\w)|(?]*+> # HTML tags\n | (?`+)([^`]|(?!(?(?!`))`)*+\\k\n # Raw\n | \\\\[\\\\`*_{}\\[\\]()#.!+\\->]?+ # Escapes\n | \\[\n (\n (? # Named group\n [^\\[\\]\\\\] # Match most chars\n | \\\\. # Escaped chars\n | \\[ \\g*+ \\] # Nested brackets\n )*+\n \\]\n (\n ( # Reference Link\n [ ]? # Optional space\n \\[[^\\]]*+\\] # Ref name\n )\n | ( # Inline Link\n \\( # Opening paren\n [ \\t]*+ # Optional whtiespace\n ? # URL\n [ \\t]*+ # Optional whtiespace\n ( # Optional Title\n (?['\"])\n (.*?)\n \\k<title>\n )?\n \\)\n )\n )\n )\n | \\k<open>\\k<open> # Must be bold closer\n | (?!(?<=\\S)\\k<open>). # Everything besides\n # style closer\n )++\n (?<=\\S)(?=_\\b|\\*)\\k<open> # Close\n )\n", + "begin": "(?x) (?<open>(\\*(?=\\w)|(?<!\\w)\\*|(?<!\\w)\\b_))(?=\\S) # Open\n (?=\n (\n <[^>]*+> # HTML tags\n | (?<raw>`+)([^`]|(?!(?<!`)\\k<raw>(?!`))`)*+\\k<raw>\n # Raw\n | \\\\[\\\\`*_{}\\[\\]()#.!+\\->]?+ # Escapes\n | \\[\n (\n (?<square> # Named group\n [^\\[\\]\\\\] # Match most chars\n | \\\\. # Escaped chars\n | \\[ \\g<square>*+ \\] # Nested brackets\n )*+\n \\]\n (\n ( # Reference Link\n [ ]? # Optional space\n \\[[^\\]]*+\\] # Ref name\n )\n | ( # Inline Link\n \\( # Opening paren\n [ \\t]*+ # Optional whtiespace\n <?(.*?)>? # URL\n [ \\t]*+ # Optional whtiespace\n ( # Optional Title\n (?<title>['\"])\n (.*?)\n \\k<title>\n )?\n \\)\n )\n )\n )\n | \\k<open>\\k<open> # Must be bold closer\n | (?!(?<=\\S)\\k<open>). # Everything besides\n # style closer\n )++\n (?<=\\S)(?=_\\b|\\*)\\k<open> # Close\n )\n", "captures": { "1": { "name": "punctuation.definition.italic.markdown" diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 8beec792f7f..8f987ab7c1f 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -356,6 +356,7 @@ "highlight.js": "^10.4.1", "markdown-it": "^12.2.0", "markdown-it-front-matter": "^0.2.1", + "morphdom": "^2.6.1", "vscode-extension-telemetry": "0.4.2", "vscode-nls": "^5.0.0" }, diff --git a/extensions/markdown-language-features/preview-src/activeLineMarker.ts b/extensions/markdown-language-features/preview-src/activeLineMarker.ts index 21f41bc675f..ec77600b609 100644 --- a/extensions/markdown-language-features/preview-src/activeLineMarker.ts +++ b/extensions/markdown-language-features/preview-src/activeLineMarker.ts @@ -7,8 +7,8 @@ import { getElementsForSourceLine } from './scroll-sync'; export class ActiveLineMarker { private _current: any; - onDidChangeTextEditorSelection(line: number) { - const { previous } = getElementsForSourceLine(line); + onDidChangeTextEditorSelection(line: number, documentVersion: number) { + const { previous } = getElementsForSourceLine(line, documentVersion); this._update(previous && previous.element); } @@ -31,4 +31,4 @@ export class ActiveLineMarker { } element.className += ' code-active-line'; } -} \ No newline at end of file +} diff --git a/extensions/markdown-language-features/preview-src/csp.ts b/extensions/markdown-language-features/preview-src/csp.ts index 5bfa557bd48..49d8cb1efe6 100644 --- a/extensions/markdown-language-features/preview-src/csp.ts +++ b/extensions/markdown-language-features/preview-src/csp.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { MessagePoster } from './messaging'; -import { getSettings } from './settings'; +import { SettingsManager } from './settings'; import { getStrings } from './strings'; /** @@ -16,7 +16,9 @@ export class CspAlerter { private messaging?: MessagePoster; - constructor() { + constructor( + private readonly settingsManager: SettingsManager, + ) { document.addEventListener('securitypolicyviolation', () => { this.onCspWarning(); }); @@ -42,7 +44,7 @@ export class CspAlerter { private showCspWarning() { const strings = getStrings(); - const settings = getSettings(); + const settings = this.settingsManager.settings; if (this.didShow || settings.disableSecurityWarnings || !this.messaging) { return; diff --git a/extensions/markdown-language-features/preview-src/index.ts b/extensions/markdown-language-features/preview-src/index.ts index 18b84b2cebb..9e0672e86af 100644 --- a/extensions/markdown-language-features/preview-src/index.ts +++ b/extensions/markdown-language-features/preview-src/index.ts @@ -7,12 +7,14 @@ import { ActiveLineMarker } from './activeLineMarker'; import { onceDocumentLoaded } from './events'; import { createPosterForVsCode } from './messaging'; import { getEditorLineNumberForPageOffset, scrollToRevealSourceLine, getLineElementForFragment } from './scroll-sync'; -import { getSettings, getData } from './settings'; +import { SettingsManager, getData } from './settings'; import throttle = require('lodash.throttle'); +import morphdom from 'morphdom'; +let documentVersion = 0; let scrollDisabledCount = 0; const marker = new ActiveLineMarker(); -const settings = getSettings(); +const settings = new SettingsManager(); const vscode = acquireVsCodeApi(); @@ -26,15 +28,11 @@ const state = { // Make sure to sync VS Code state here vscode.setState(state); -const messaging = createPosterForVsCode(vscode); +const messaging = createPosterForVsCode(vscode, settings); window.cspAlerter.setPoster(messaging); window.styleLoadingMonitor.setPoster(messaging); -window.onload = () => { - updateImageSizes(); -}; - function doAfterImagesLoaded(cb: () => void) { const imgElements = document.getElementsByTagName('img'); @@ -58,7 +56,7 @@ function doAfterImagesLoaded(cb: () => void) { onceDocumentLoaded(() => { const scrollProgress = state.scrollProgress; - if (typeof scrollProgress === 'number' && !settings.fragment) { + if (typeof scrollProgress === 'number' && !settings.settings.fragment) { doAfterImagesLoaded(() => { scrollDisabledCount += 1; window.scrollTo(0, scrollProgress * document.body.clientHeight); @@ -66,22 +64,22 @@ onceDocumentLoaded(() => { return; } - if (settings.scrollPreviewWithEditor) { + if (settings.settings.scrollPreviewWithEditor) { doAfterImagesLoaded(() => { // Try to scroll to fragment if available - if (settings.fragment) { + if (settings.settings.fragment) { state.fragment = undefined; vscode.setState(state); - const element = getLineElementForFragment(settings.fragment); + const element = getLineElementForFragment(settings.settings.fragment, documentVersion); if (element) { scrollDisabledCount += 1; - scrollToRevealSourceLine(element.line); + scrollToRevealSourceLine(element.line, documentVersion, settings); } } else { - if (!isNaN(settings.line!)) { + if (!isNaN(settings.settings.line!)) { scrollDisabledCount += 1; - scrollToRevealSourceLine(settings.line!); + scrollToRevealSourceLine(settings.settings.line!, documentVersion, settings); } } }); @@ -91,7 +89,7 @@ onceDocumentLoaded(() => { const onUpdateView = (() => { const doScroll = throttle((line: number) => { scrollDisabledCount += 1; - doAfterImagesLoaded(() => scrollToRevealSourceLine(line)); + doAfterImagesLoaded(() => scrollToRevealSourceLine(line, documentVersion, settings)); }, 50); return (line: number) => { @@ -103,53 +101,38 @@ const onUpdateView = (() => { }; })(); -let updateImageSizes = throttle(() => { - const imageInfo: { id: string, height: number, width: number; }[] = []; - let images = document.getElementsByTagName('img'); - if (images) { - let i; - for (i = 0; i < images.length; i++) { - const img = images[i]; - - if (img.classList.contains('loading')) { - img.classList.remove('loading'); - } - - imageInfo.push({ - id: img.id, - height: img.height, - width: img.width - }); - } - - messaging.postMessage('cacheImageSizes', imageInfo); - } -}, 50); window.addEventListener('resize', () => { scrollDisabledCount += 1; updateScrollProgress(); - updateImageSizes(); }, true); window.addEventListener('message', event => { - if (event.data.source !== settings.source) { + if (settings.settings && event.data.source !== settings.settings.source) { return; } switch (event.data.type) { case 'onDidChangeTextEditorSelection': - marker.onDidChangeTextEditorSelection(event.data.line); + marker.onDidChangeTextEditorSelection(event.data.line, documentVersion); break; case 'updateView': onUpdateView(event.data.line); break; + + case 'updateContent': + const root = document.querySelector('.markdown-body')!; + morphdom(root, event.data.content); + ++documentVersion; + + window.dispatchEvent(new CustomEvent('vscode.markdown.updateContent')); + break; } }, false); document.addEventListener('dblclick', event => { - if (!settings.doubleClickToSwitchToEditor) { + if (!settings.settings.doubleClickToSwitchToEditor) { return; } @@ -161,7 +144,7 @@ document.addEventListener('dblclick', event => { } const offset = event.pageY; - const line = getEditorLineNumberForPageOffset(offset); + const line = getEditorLineNumberForPageOffset(offset, documentVersion, settings); if (typeof line === 'number' && !isNaN(line)) { messaging.postMessage('didClick', { line: Math.floor(line) }); } @@ -210,7 +193,7 @@ window.addEventListener('scroll', throttle(() => { if (scrollDisabledCount > 0) { scrollDisabledCount -= 1; } else { - const line = getEditorLineNumberForPageOffset(window.scrollY); + const line = getEditorLineNumberForPageOffset(window.scrollY, documentVersion, settings); if (typeof line === 'number' && !isNaN(line)) { messaging.postMessage('revealLine', { line }); } diff --git a/extensions/markdown-language-features/preview-src/messaging.ts b/extensions/markdown-language-features/preview-src/messaging.ts index bfb1d258ee5..6452ae6c3c8 100644 --- a/extensions/markdown-language-features/preview-src/messaging.ts +++ b/extensions/markdown-language-features/preview-src/messaging.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getSettings } from './settings'; +import { SettingsManager } from './settings'; export interface MessagePoster { /** @@ -12,12 +12,12 @@ export interface MessagePoster { postMessage(type: string, body: object): void; } -export const createPosterForVsCode = (vscode: any) => { +export const createPosterForVsCode = (vscode: any, settingsManager: SettingsManager) => { return new class implements MessagePoster { postMessage(type: string, body: object): void { vscode.postMessage({ type, - source: getSettings().source, + source: settingsManager.settings!.source, body }); } diff --git a/extensions/markdown-language-features/preview-src/pre.ts b/extensions/markdown-language-features/preview-src/pre.ts index 9f1c806a979..5de29c0de2b 100644 --- a/extensions/markdown-language-features/preview-src/pre.ts +++ b/extensions/markdown-language-features/preview-src/pre.ts @@ -5,6 +5,7 @@ import { CspAlerter } from './csp'; import { StyleLoadingMonitor } from './loading'; +import { SettingsManager } from './settings'; declare global { interface Window { @@ -13,5 +14,5 @@ declare global { } } -window.cspAlerter = new CspAlerter(); -window.styleLoadingMonitor = new StyleLoadingMonitor(); \ No newline at end of file +window.cspAlerter = new CspAlerter(new SettingsManager()); +window.styleLoadingMonitor = new StyleLoadingMonitor(); diff --git a/extensions/markdown-language-features/preview-src/scroll-sync.ts b/extensions/markdown-language-features/preview-src/scroll-sync.ts index 4d1adcfb35c..fdeb1a5ae78 100644 --- a/extensions/markdown-language-features/preview-src/scroll-sync.ts +++ b/extensions/markdown-language-features/preview-src/scroll-sync.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getSettings } from './settings'; +import { SettingsManager } from './settings'; const codeLineClass = 'code-line'; @@ -11,8 +11,8 @@ function clamp(min: number, max: number, value: number) { return Math.min(max, Math.max(min, value)); } -function clampLine(line: number) { - return clamp(0, getSettings().lineCount - 1, line); +function clampLine(line: number, lineCount: number) { + return clamp(0, lineCount - 1, line); } @@ -22,10 +22,12 @@ export interface CodeLineElement { } const getCodeLineElements = (() => { - let elements: CodeLineElement[]; - return () => { - if (!elements) { - elements = [{ element: document.body, line: 0 }]; + let cachedElements: CodeLineElement[] | undefined; + let cachedVersion = -1; + return (documentVersion: number) => { + if (!cachedElements || documentVersion !== cachedVersion) { + cachedVersion = documentVersion; + cachedElements = [{ element: document.body, line: 0 }]; for (const element of document.getElementsByClassName(codeLineClass)) { const line = +element.getAttribute('data-line')!; if (isNaN(line)) { @@ -35,13 +37,13 @@ const getCodeLineElements = (() => { if (element.tagName === 'CODE' && element.parentElement && element.parentElement.tagName === 'PRE') { // Fenched code blocks are a special case since the `code-line` can only be marked on // the `<code>` element and not the parent `<pre>` element. - elements.push({ element: element.parentElement as HTMLElement, line }); + cachedElements.push({ element: element.parentElement as HTMLElement, line }); } else { - elements.push({ element: element as HTMLElement, line }); + cachedElements.push({ element: element as HTMLElement, line }); } } } - return elements; + return cachedElements; }; })(); @@ -51,9 +53,9 @@ const getCodeLineElements = (() => { * If an exact match, returns a single element. If the line is between elements, * returns the element prior to and the element after the given line. */ -export function getElementsForSourceLine(targetLine: number): { previous: CodeLineElement; next?: CodeLineElement; } { +export function getElementsForSourceLine(targetLine: number, documentVersion: number): { previous: CodeLineElement; next?: CodeLineElement; } { const lineNumber = Math.floor(targetLine); - const lines = getCodeLineElements(); + const lines = getCodeLineElements(documentVersion); let previous = lines[0] || null; for (const entry of lines) { if (entry.line === lineNumber) { @@ -69,8 +71,8 @@ export function getElementsForSourceLine(targetLine: number): { previous: CodeLi /** * Find the html elements that are at a specific pixel offset on the page. */ -export function getLineElementsAtPageOffset(offset: number): { previous: CodeLineElement; next?: CodeLineElement; } { - const lines = getCodeLineElements(); +export function getLineElementsAtPageOffset(offset: number, documentVersion: number): { previous: CodeLineElement; next?: CodeLineElement; } { + const lines = getCodeLineElements(documentVersion); const position = offset - window.scrollY; let lo = -1; let hi = lines.length - 1; @@ -117,8 +119,8 @@ function getElementBounds({ element }: CodeLineElement): { top: number, height: /** * Attempt to reveal the element for a source line in the editor. */ -export function scrollToRevealSourceLine(line: number) { - if (!getSettings().scrollPreviewWithEditor) { +export function scrollToRevealSourceLine(line: number, documentVersion: number, settingsManager: SettingsManager) { + if (!settingsManager.settings?.scrollPreviewWithEditor) { return; } @@ -127,7 +129,7 @@ export function scrollToRevealSourceLine(line: number) { return; } - const { previous, next } = getElementsForSourceLine(line); + const { previous, next } = getElementsForSourceLine(line, documentVersion); if (!previous) { return; } @@ -147,19 +149,20 @@ export function scrollToRevealSourceLine(line: number) { window.scroll(window.scrollX, Math.max(1, window.scrollY + scrollTo)); } -export function getEditorLineNumberForPageOffset(offset: number) { - const { previous, next } = getLineElementsAtPageOffset(offset); +export function getEditorLineNumberForPageOffset(offset: number, documentVersion: number, settingsManager: SettingsManager) { + const lineCount = settingsManager.settings?.lineCount ?? 0; + const { previous, next } = getLineElementsAtPageOffset(offset, documentVersion); if (previous) { const previousBounds = getElementBounds(previous); const offsetFromPrevious = (offset - window.scrollY - previousBounds.top); if (next) { const progressBetweenElements = offsetFromPrevious / (getElementBounds(next).top - previousBounds.top); const line = previous.line + progressBetweenElements * (next.line - previous.line); - return clampLine(line); + return clampLine(line, lineCount); } else { const progressWithinElement = offsetFromPrevious / (previousBounds.height); const line = previous.line + progressWithinElement; - return clampLine(line); + return clampLine(line, lineCount); } } return null; @@ -168,8 +171,8 @@ export function getEditorLineNumberForPageOffset(offset: number) { /** * Try to find the html element by using a fragment id */ -export function getLineElementForFragment(fragment: string): CodeLineElement | undefined { - return getCodeLineElements().find((element) => { +export function getLineElementForFragment(fragment: string, documentVersion: number): CodeLineElement | undefined { + return getCodeLineElements(documentVersion).find((element) => { return element.element.id === fragment; }); } diff --git a/extensions/markdown-language-features/preview-src/settings.ts b/extensions/markdown-language-features/preview-src/settings.ts index 470246f94c1..a1212fdf432 100644 --- a/extensions/markdown-language-features/preview-src/settings.ts +++ b/extensions/markdown-language-features/preview-src/settings.ts @@ -15,8 +15,6 @@ export interface PreviewSettings { readonly webviewResourceRoot: string; } -let cachedSettings: PreviewSettings | undefined = undefined; - export function getData<T = {}>(key: string): T { const element = document.getElementById('vscode-markdown-preview-data'); if (element) { @@ -29,15 +27,14 @@ export function getData<T = {}>(key: string): T { throw new Error(`Could not load data for ${key}`); } -export function getSettings(): PreviewSettings { - if (cachedSettings) { - return cachedSettings; +export class SettingsManager { + private _settings: PreviewSettings = getData('data-settings'); + + public get settings(): PreviewSettings { + return this._settings; } - cachedSettings = getData('data-settings'); - if (cachedSettings) { - return cachedSettings; + public updateSettings(newSettings: PreviewSettings) { + this._settings = newSettings; } - - throw new Error('Could not load settings'); } diff --git a/extensions/markdown-language-features/preview-src/tsconfig.json b/extensions/markdown-language-features/preview-src/tsconfig.json index 62af34c62f8..c12ff006ed8 100644 --- a/extensions/markdown-language-features/preview-src/tsconfig.json +++ b/extensions/markdown-language-features/preview-src/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "outDir": "./dist/", "jsx": "react", + "esModuleInterop": true, "lib": [ "es2018", "DOM", diff --git a/extensions/markdown-language-features/src/features/documentLinkProvider.ts b/extensions/markdown-language-features/src/features/documentLinkProvider.ts index e2bd61b2e9d..f80664cd59e 100644 --- a/extensions/markdown-language-features/src/features/documentLinkProvider.ts +++ b/extensions/markdown-language-features/src/features/documentLinkProvider.ts @@ -91,19 +91,21 @@ function extractDocumentLink( } } -/* Used to strip brackets from the markdown link - <http://example.com> will be transformed to - http://example.com +const angleBracketLinkRe = /^<(.*)>$/; + +/** + * Used to strip brackets from the markdown link + * + * <http://example.com> will be transformed to http://example.com */ export function stripAngleBrackets(link: string) { - const bracketMatcher = /^<(.*)>$/; - return link.replace(bracketMatcher, '$1'); + return link.replace(angleBracketLinkRe, '$1'); } export default class LinkProvider implements vscode.DocumentLinkProvider { private readonly linkPattern = /(\[((!\[[^\]]*?\]\(\s*)([^\s\(\)]+?)\s*\)\]|(?:\\\]|[^\]])*\])\(\s*)(([^\s\(\)]|\([^\s\(\)]*?\))+)\s*(".*?")?\)/g; private readonly referenceLinkPattern = /(\[((?:\\\]|[^\]])+)\]\[\s*?)([^\s\]]*?)\]/g; - private readonly definitionPattern = /^([\t ]*\[(?!\^)((?:\\\]|[^\]])+)\]:\s*)(\S+)/gm; + private readonly definitionPattern = /^([\t ]*\[(?!\^)((?:\\\]|[^\]])+)\]:\s*)([^<]\S*|<[^>]+>)/gm; public provideDocumentLinks( document: vscode.TextDocument, @@ -192,15 +194,23 @@ export default class LinkProvider implements vscode.DocumentLinkProvider { const pre = match[1]; const reference = match[2]; const link = match[3].trim(); - const offset = (match.index || 0) + pre.length; - const linkStart = document.positionAt(offset); - const linkEnd = document.positionAt(offset + link.length); - out.set(reference, { - link: link, - linkRange: new vscode.Range(linkStart, linkEnd) - }); + if (angleBracketLinkRe.test(link)) { + const linkStart = document.positionAt(offset + 1); + const linkEnd = document.positionAt(offset + link.length - 1); + out.set(reference, { + link: link.substring(1, link.length - 1), + linkRange: new vscode.Range(linkStart, linkEnd) + }); + } else { + const linkStart = document.positionAt(offset); + const linkEnd = document.positionAt(offset + link.length); + out.set(reference, { + link: link, + linkRange: new vscode.Range(linkStart, linkEnd) + }); + } } return out; } diff --git a/extensions/markdown-language-features/src/features/preview.ts b/extensions/markdown-language-features/src/features/preview.ts index 4d172a3306f..3009c910855 100644 --- a/extensions/markdown-language-features/src/features/preview.ts +++ b/extensions/markdown-language-features/src/features/preview.ts @@ -16,7 +16,7 @@ import { WebviewResourceProvider } from '../util/resources'; import { getVisibleLine, LastScrollLocation, TopmostLineMonitor } from '../util/topmostLineMonitor'; import { urlToUri } from '../util/url'; import { MarkdownPreviewConfigurationManager } from './previewConfig'; -import { MarkdownContentProvider, MarkdownContentProviderOutput } from './previewContentProvider'; +import { MarkdownContentProvider } from './previewContentProvider'; const localize = nls.loadMessageBundle(); @@ -63,7 +63,7 @@ interface PreviewStyleLoadErrorMessage extends WebviewMessage { export class PreviewDocumentVersion { - private readonly resource: vscode.Uri; + public readonly resource: vscode.Uri; private readonly version: number; public constructor(document: vscode.TextDocument) { @@ -314,13 +314,18 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { return; } + const shouldReloadPage = !this.currentVersion || this.currentVersion.resource.toString() !== pendingVersion.resource.toString(); this.currentVersion = pendingVersion; - const content = await this._contentProvider.provideTextDocumentContent(document, this, this._previewConfigurations, this.line, this.state); + + const content = await (shouldReloadPage + ? this._contentProvider.provideTextDocumentContent(document, this, this._previewConfigurations, this.line, this.state) + : this._contentProvider.markdownBody(document, this)); // Another call to `doUpdate` may have happened. // Make sure we are still updating for the correct document if (this.currentVersion?.equals(pendingVersion)) { - this.setContent(content); + this.updateWebviewContent(content.html, shouldReloadPage); + this.updateImageWatchers(content.containingImages); } } @@ -366,7 +371,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { this._webviewPanel.webview.html = this._contentProvider.provideFileNotFoundContent(this._resource); } - private setContent(content: MarkdownContentProviderOutput): void { + private updateWebviewContent(html: string, reloadPage: boolean): void { if (this._disposed) { return; } @@ -377,9 +382,19 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { this._webviewPanel.iconPath = this.iconPath; this._webviewPanel.webview.options = this.getWebviewOptions(); - this._webviewPanel.webview.html = content.html; + if (reloadPage) { + this._webviewPanel.webview.html = html; + } else { + this._webviewPanel.webview.postMessage({ + type: 'updateContent', + content: html, + source: this._resource.toString(), + }); + } + } - const srcs = new Set(content.containingImages.map(img => img.src)); + private updateImageWatchers(containingImages: { src: string }[]) { + const srcs = new Set(containingImages.map(img => img.src)); // Delete stale file watchers. for (const [src, watcher] of [...this._fileWatchersBySrc]) { diff --git a/extensions/markdown-language-features/src/features/previewContentProvider.ts b/extensions/markdown-language-features/src/features/previewContentProvider.ts index 5ba85bdc4d9..6d86de5351f 100644 --- a/extensions/markdown-language-features/src/features/previewContentProvider.ts +++ b/extensions/markdown-language-features/src/features/previewContentProvider.ts @@ -81,7 +81,7 @@ export class MarkdownContentProvider { const nonce = getNonce(); const csp = this.getCsp(resourceProvider, sourceUri, nonce); - const body = await this.engine.render(markdownDocument, resourceProvider); + const body = await this.markdownBody(markdownDocument, resourceProvider); const html = `<!DOCTYPE html> <html style="${escapeAttribute(this.getSettingsOverrideStyles(config))}"> <head> @@ -97,7 +97,6 @@ export class MarkdownContentProvider { </head> <body class="vscode-body ${config.scrollBeyondLastLine ? 'scrollBeyondLastLine' : ''} ${config.wordWrap ? 'wordWrap' : ''} ${config.markEditorSelection ? 'showEditorSelection' : ''}"> ${body.html} - <div class="code-line" data-line="${markdownDocument.lineCount}"></div> ${this.getScripts(resourceProvider, nonce)} </body> </html>`; @@ -107,6 +106,18 @@ export class MarkdownContentProvider { }; } + public async markdownBody( + markdownDocument: vscode.TextDocument, + resourceProvider: WebviewResourceProvider, + ): Promise<MarkdownContentProviderOutput> { + const rendered = await this.engine.render(markdownDocument, resourceProvider); + const html = `<div class="markdown-body">${rendered.html}<div class="code-line" data-line="${markdownDocument.lineCount}"></div></div>`; + return { + html, + containingImages: rendered.containingImages + }; + } + public provideFileNotFoundContent( resource: vscode.Uri, ): string { diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index abfae361cf2..29d5274dc41 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -112,6 +112,7 @@ export class MarkdownEngine { this.md = (async () => { const markdownIt = await import('markdown-it'); let md: MarkdownIt = markdownIt(await getMarkdownOptions(() => md)); + md.linkify.set({ fuzzyLink: false }); for (const plugin of this.contributionProvider.contributions.markdownItPlugins.values()) { try { diff --git a/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts b/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts index fbcaf251204..4c39b57b4cb 100644 --- a/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts +++ b/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts @@ -139,11 +139,22 @@ suite('markdown.DocumentLinkProvider', () => { } }); - // #107471 - test('Should not consider link references starting with ^ character valid', () => { + test('Should not consider link references starting with ^ character valid (#107471)', () => { const links = getLinksForFile('[^reference]: https://example.com'); assert.strictEqual(links.length, 0); }); + + test('Should find definitions links with spaces in angle brackets (#136073)', () => { + const links = getLinksForFile([ + '[a]: <b c>', + '[b]: <cd>', + ].join('\n')); + assert.strictEqual(links.length, 2); + + const [link1, link2] = links; + assertRangeEqual(link1.range, new vscode.Range(0, 6, 0, 9)); + assertRangeEqual(link2.range, new vscode.Range(1, 6, 1, 8)); + }); }); diff --git a/extensions/markdown-language-features/src/test/inMemoryDocument.ts b/extensions/markdown-language-features/src/test/inMemoryDocument.ts index e9629f1fbab..bb7d83907f8 100644 --- a/extensions/markdown-language-features/src/test/inMemoryDocument.ts +++ b/extensions/markdown-language-features/src/test/inMemoryDocument.ts @@ -16,7 +16,6 @@ export class InMemoryDocument implements vscode.TextDocument { this._lines = this._contents.split(/\r\n|\n/g); } - isUntitled: boolean = false; languageId: string = ''; isDirty: boolean = false; @@ -49,7 +48,7 @@ export class InMemoryDocument implements vscode.TextDocument { const before = this._contents.slice(0, offset); const newLines = before.match(/\r\n|\n/g); const line = newLines ? newLines.length : 0; - const preCharacters = before.match(/(\r\n|\n|^).*$/g); + const preCharacters = before.match(/(?<=\r\n|\n|^).*$/g); return new vscode.Position(line, preCharacters ? preCharacters[0].length : 0); } getText(_range?: vscode.Range | undefined): string { diff --git a/extensions/markdown-language-features/yarn.lock b/extensions/markdown-language-features/yarn.lock index 17321f36d85..1200fc4c7d5 100644 --- a/extensions/markdown-language-features/yarn.lock +++ b/extensions/markdown-language-features/yarn.lock @@ -107,6 +107,11 @@ mdurl@^1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= +morphdom@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/morphdom/-/morphdom-2.6.1.tgz#e868e24f989fa3183004b159aed643e628b4306e" + integrity sha512-Y8YRbAEP3eKykroIBWrjcfMw7mmwJfjhqdpSvoqinu8Y702nAwikpXcNFDiIkyvfCLxLM9Wu95RZqo4a9jFBaA== + uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index f4caacd2a15..9912c4944a6 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -710,7 +710,7 @@ export class AzureActiveDirectoryService { if (this._tokens.length === 0) { await this._keychain.deleteToken(); } else { - this.storeTokenData(); + await this.storeTokenData(); } return session; diff --git a/extensions/objective-c/cgmanifest.json b/extensions/objective-c/cgmanifest.json index d246105f7f0..b46a4519f3f 100644 --- a/extensions/objective-c/cgmanifest.json +++ b/extensions/objective-c/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "jeff-hykin/cpp-textmate-grammar", "repositoryUrl": "https://github.com/jeff-hykin/cpp-textmate-grammar", - "commitHash": "11b4b4e2abf581d0f3c6c90ac6632d1b2f974c67" + "commitHash": "0ef79f098ed80ce5a86be4ed40f99ebcdbac4895" } }, "license": "MIT", diff --git a/extensions/objective-c/syntaxes/objective-c++.tmLanguage.json b/extensions/objective-c/syntaxes/objective-c++.tmLanguage.json index cf91d6328b8..da1ebc1eb0e 100644 --- a/extensions/objective-c/syntaxes/objective-c++.tmLanguage.json +++ b/extensions/objective-c/syntaxes/objective-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/11b4b4e2abf581d0f3c6c90ac6632d1b2f974c67", + "version": "https://github.com/jeff-hykin/cpp-textmate-grammar/commit/0ef79f098ed80ce5a86be4ed40f99ebcdbac4895", "name": "Objective-C++", "scopeName": "source.objcpp", "patterns": [ diff --git a/extensions/objective-c/syntaxes/objective-c.tmLanguage.json b/extensions/objective-c/syntaxes/objective-c.tmLanguage.json index fc87a93e402..27901fd2bcd 100644 --- a/extensions/objective-c/syntaxes/objective-c.tmLanguage.json +++ b/extensions/objective-c/syntaxes/objective-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/11b4b4e2abf581d0f3c6c90ac6632d1b2f974c67", + "version": "https://github.com/jeff-hykin/cpp-textmate-grammar/commit/0ef79f098ed80ce5a86be4ed40f99ebcdbac4895", "name": "Objective-C", "scopeName": "source.objc", "patterns": [ diff --git a/extensions/package.json b/extensions/package.json index 848eaa085f2..656a2a688d5 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -4,7 +4,7 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "4.4.3" + "typescript": "^4.5.0-dev.20211029" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/powershell/cgmanifest.json b/extensions/powershell/cgmanifest.json index 13eee1dbe4e..61f29281a5e 100644 --- a/extensions/powershell/cgmanifest.json +++ b/extensions/powershell/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "PowerShell/EditorSyntax", "repositoryUrl": "https://github.com/PowerShell/EditorSyntax", - "commitHash": "c150c15cca30cafd2159e3f53514f93ccf4c5649" + "commitHash": "742f0b5d4b60f5930c0b47fcc1f646860521296e" } }, "license": "MIT", diff --git a/extensions/powershell/syntaxes/powershell.tmLanguage.json b/extensions/powershell/syntaxes/powershell.tmLanguage.json index 61267a44de1..e70dd5309d0 100644 --- a/extensions/powershell/syntaxes/powershell.tmLanguage.json +++ b/extensions/powershell/syntaxes/powershell.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/PowerShell/EditorSyntax/commit/c150c15cca30cafd2159e3f53514f93ccf4c5649", + "version": "https://github.com/PowerShell/EditorSyntax/commit/742f0b5d4b60f5930c0b47fcc1f646860521296e", "name": "PowerShell", "scopeName": "source.powershell", "patterns": [ @@ -190,7 +190,7 @@ "name": "support.function.powershell" }, { - "match": "(?<!\\w|-|\\.)((?i:begin|break|catch|continue|data|default|define|do|dynamicparam|else|elseif|end|exit|finally|for|from|if|in|inlinescript|parallel|param|process|return|sequence|switch|throw|trap|try|until|var|while)|%|\\?)(?!\\w)", + "match": "(?<!\\w|-|\\.)((?i:begin|break|catch|clean|continue|data|default|define|do|dynamicparam|else|elseif|end|exit|finally|for|from|if|in|inlinescript|parallel|param|process|return|sequence|switch|throw|trap|try|until|var|while)|%|\\?)(?!\\w)", "name": "keyword.control.powershell" }, { diff --git a/extensions/r/cgmanifest.json b/extensions/r/cgmanifest.json index 9a2c1398253..08eb7961020 100644 --- a/extensions/r/cgmanifest.json +++ b/extensions/r/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "Ikuyadeu/vscode-R", "repositoryUrl": "https://github.com/Ikuyadeu/vscode-R", - "commitHash": "c6a9803fbda262ea68c427a2339bddafed41a9d5" + "commitHash": "c9290464add8409f35fcbe47339d4b2ca0e4416c" } }, "license": "MIT", - "version": "2.1.0" + "version": "2.3.2" } ], "version": 1 diff --git a/extensions/r/syntaxes/r.tmLanguage.json b/extensions/r/syntaxes/r.tmLanguage.json index d2186b9c5ff..4f4bb06f1b0 100644 --- a/extensions/r/syntaxes/r.tmLanguage.json +++ b/extensions/r/syntaxes/r.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/Ikuyadeu/vscode-R/commit/c6a9803fbda262ea68c427a2339bddafed41a9d5", + "version": "https://github.com/Ikuyadeu/vscode-R/commit/c9290464add8409f35fcbe47339d4b2ca0e4416c", "name": "R", "scopeName": "source.r", "patterns": [ @@ -459,7 +459,7 @@ "roxygen": { "patterns": [ { - "begin": "^(#')\\s*", + "begin": "^\\s*(#')\\s*", "beginCaptures": { "1": { "name": "punctuation.definition.comment.r" diff --git a/extensions/typescript-basics/cgmanifest.json b/extensions/typescript-basics/cgmanifest.json index e8d888a5a01..fedaa286599 100644 --- a/extensions/typescript-basics/cgmanifest.json +++ b/extensions/typescript-basics/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "TypeScript-TmLanguage", "repositoryUrl": "https://github.com/microsoft/TypeScript-TmLanguage", - "commitHash": "33d8371c344f0b54746586313a939f742f9bcd3a" + "commitHash": "644389aef914fc6fbc97a4dd799cc2d1431ffc87" } }, "license": "MIT", diff --git a/extensions/typescript-basics/package.json b/extensions/typescript-basics/package.json index b3c1b42f7e4..0f51d88cf07 100644 --- a/extensions/typescript-basics/package.json +++ b/extensions/typescript-basics/package.json @@ -60,6 +60,8 @@ "path": "./syntaxes/TypeScript.tmLanguage.json", "tokenTypes": { "meta.template.expression": "other", + "meta.template.expression string": "string", + "meta.template.expression comment": "comment", "entity.name.type.instance.jsdoc": "other", "entity.name.function.tagged-template": "other", "meta.import string.quoted": "other", @@ -78,6 +80,8 @@ }, "tokenTypes": { "meta.template.expression": "other", + "meta.template.expression string": "string", + "meta.template.expression comment": "comment", "entity.name.type.instance.jsdoc": "other", "entity.name.function.tagged-template": "other", "meta.import string.quoted": "other", diff --git a/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json b/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json index b1660943b90..6237552fb08 100644 --- a/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json +++ b/extensions/typescript-basics/syntaxes/TypeScript.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/microsoft/TypeScript-TmLanguage/commit/33d8371c344f0b54746586313a939f742f9bcd3a", + "version": "https://github.com/microsoft/TypeScript-TmLanguage/commit/644389aef914fc6fbc97a4dd799cc2d1431ffc87", "name": "TypeScript", "scopeName": "source.ts", "patterns": [ @@ -2299,24 +2299,27 @@ "include": "#comment" }, { - "match": "(?<![_$[:alnum:]])(?:(?<=\\.\\.\\.)|(?<!\\.))(?:(\\bdefault)|(\\*)|(\\b[_$[:alpha:]][_$[:alnum:]]*))\\s+(as)\\s+(?:(default(?![_$[:alnum:]])(?:(?=\\.\\.\\.)|(?!\\.)))|([_$[:alpha:]][_$[:alnum:]]*))", + "match": "(?<![_$[:alnum:]])(?:(?<=\\.\\.\\.)|(?<!\\.))(?:(?:(\\btype)\\s+)?(?:(\\bdefault)|(\\*)|(\\b[_$[:alpha:]][_$[:alnum:]]*)))\\s+(as)\\s+(?:(default(?![_$[:alnum:]])(?:(?=\\.\\.\\.)|(?!\\.)))|([_$[:alpha:]][_$[:alnum:]]*))", "captures": { "1": { - "name": "keyword.control.default.ts" + "name": "keyword.control.type.ts" }, "2": { - "name": "constant.language.import-export-all.ts" - }, - "3": { - "name": "variable.other.readwrite.ts" - }, - "4": { - "name": "keyword.control.as.ts" - }, - "5": { "name": "keyword.control.default.ts" }, + "3": { + "name": "constant.language.import-export-all.ts" + }, + "4": { + "name": "variable.other.readwrite.ts" + }, + "5": { + "name": "keyword.control.as.ts" + }, "6": { + "name": "keyword.control.default.ts" + }, + "7": { "name": "variable.other.readwrite.alias.ts" } } @@ -2333,8 +2336,15 @@ "match": "\\b(default)\\b" }, { - "name": "variable.other.readwrite.alias.ts", - "match": "([_$[:alpha:]][_$[:alnum:]]*)" + "match": "(?:(\\btype)\\s+)?([_$[:alpha:]][_$[:alnum:]]*)", + "captures": { + "1": { + "name": "keyword.control.type.ts" + }, + "2": { + "name": "variable.other.readwrite.alias.ts" + } + } } ] }, diff --git a/extensions/typescript-basics/syntaxes/TypeScriptReact.tmLanguage.json b/extensions/typescript-basics/syntaxes/TypeScriptReact.tmLanguage.json index 1f93e7d4fb2..86fae5fa912 100644 --- a/extensions/typescript-basics/syntaxes/TypeScriptReact.tmLanguage.json +++ b/extensions/typescript-basics/syntaxes/TypeScriptReact.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/microsoft/TypeScript-TmLanguage/commit/33d8371c344f0b54746586313a939f742f9bcd3a", + "version": "https://github.com/microsoft/TypeScript-TmLanguage/commit/644389aef914fc6fbc97a4dd799cc2d1431ffc87", "name": "TypeScriptReact", "scopeName": "source.tsx", "patterns": [ @@ -2302,24 +2302,27 @@ "include": "#comment" }, { - "match": "(?<![_$[:alnum:]])(?:(?<=\\.\\.\\.)|(?<!\\.))(?:(\\bdefault)|(\\*)|(\\b[_$[:alpha:]][_$[:alnum:]]*))\\s+(as)\\s+(?:(default(?![_$[:alnum:]])(?:(?=\\.\\.\\.)|(?!\\.)))|([_$[:alpha:]][_$[:alnum:]]*))", + "match": "(?<![_$[:alnum:]])(?:(?<=\\.\\.\\.)|(?<!\\.))(?:(?:(\\btype)\\s+)?(?:(\\bdefault)|(\\*)|(\\b[_$[:alpha:]][_$[:alnum:]]*)))\\s+(as)\\s+(?:(default(?![_$[:alnum:]])(?:(?=\\.\\.\\.)|(?!\\.)))|([_$[:alpha:]][_$[:alnum:]]*))", "captures": { "1": { - "name": "keyword.control.default.tsx" + "name": "keyword.control.type.tsx" }, "2": { - "name": "constant.language.import-export-all.tsx" - }, - "3": { - "name": "variable.other.readwrite.tsx" - }, - "4": { - "name": "keyword.control.as.tsx" - }, - "5": { "name": "keyword.control.default.tsx" }, + "3": { + "name": "constant.language.import-export-all.tsx" + }, + "4": { + "name": "variable.other.readwrite.tsx" + }, + "5": { + "name": "keyword.control.as.tsx" + }, "6": { + "name": "keyword.control.default.tsx" + }, + "7": { "name": "variable.other.readwrite.alias.tsx" } } @@ -2336,8 +2339,15 @@ "match": "\\b(default)\\b" }, { - "name": "variable.other.readwrite.alias.tsx", - "match": "([_$[:alpha:]][_$[:alnum:]]*)" + "match": "(?:(\\btype)\\s+)?([_$[:alpha:]][_$[:alnum:]]*)", + "captures": { + "1": { + "name": "keyword.control.type.tsx" + }, + "2": { + "name": "variable.other.readwrite.alias.tsx" + } + } } ] }, diff --git a/extensions/typescript-language-features/language-configuration.json b/extensions/typescript-language-features/language-configuration.json deleted file mode 100644 index 78c32ffc081..00000000000 --- a/extensions/typescript-language-features/language-configuration.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "comments": { - "lineComment": "//", - "blockComment": [ "/*", "*/" ] - }, - "brackets": [ - ["${", "}"], - ["{", "}"], - ["[", "]"], - ["(", ")"] - ], - "autoClosingPairs": [ - { "open": "{", "close": "}" }, - { "open": "[", "close": "]" }, - { "open": "(", "close": ")" }, - { "open": "'", "close": "'", "notIn": ["string", "comment"] }, - { "open": "\"", "close": "\"", "notIn": ["string"] }, - { "open": "`", "close": "`", "notIn": ["string", "comment"] }, - { "open": "/**", "close": " */", "notIn": ["string"] } - ], - "surroundingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - ["'", "'"], - ["\"", "\""], - ["`", "`"] - ], - "folding": { - "markers": { - "start": "^\\s*//\\s*#?region\\b", - "end": "^\\s*//\\s*#?endregion\\b" - } - } -} diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 888c8f39475..6f3222da323 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -1110,6 +1110,18 @@ "default": "allOpenProjects", "markdownDescription": "%typescript.workspaceSymbols.scope%", "scope": "window" + }, + "javascript.suggest.includeCompletionsWithClassMemberSnippets": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.includeCompletionsWithClassMemberSnippets%", + "scope": "resource" + }, + "typescript.suggest.includeCompletionsWithClassMemberSnippets": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.includeCompletionsWithClassMemberSnippets%", + "scope": "resource" } } }, diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index a2828b86553..c384bb3b02a 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -183,5 +183,6 @@ "codeActions.refactor.rewrite.property.generateAccessors.title": "Generate accessors", "codeActions.refactor.rewrite.property.generateAccessors.description": "Generate 'get' and 'set' accessors", "codeActions.source.organizeImports.title": "Organize imports", - "typescript.findAllFileReferences": "Find File References" + "typescript.findAllFileReferences": "Find File References", + "configuration.suggest.includeCompletionsWithClassMemberSnippets": "Enable/disable snippet completions for class members. Requires using TypeScript 4.5+ in the workspace" } diff --git a/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts index 2a987c2e95d..369136f378b 100644 --- a/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts +++ b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts @@ -13,20 +13,6 @@ import { isTypeScriptDocument } from '../utils/languageModeIds'; import { equals } from '../utils/objects'; import { ResourceMap } from '../utils/resourceMap'; -namespace ExperimentalProto { - export interface UserPreferences extends Proto.UserPreferences { - displayPartsForJSDoc: true - - includeInlayParameterNameHints?: 'none' | 'literals' | 'all'; - includeInlayParameterNameHintsWhenArgumentMatchesName?: boolean; - includeInlayFunctionParameterTypeHints?: boolean; - includeInlayVariableTypeHints?: boolean; - includeInlayPropertyDeclarationTypeHints?: boolean; - includeInlayFunctionLikeReturnTypeHints?: boolean; - includeInlayEnumMemberValueHints?: boolean; - } -} - interface FileConfiguration { readonly formatOptions: Proto.FormatCodeSettings; readonly preferences: Proto.UserPreferences; @@ -187,11 +173,10 @@ export default class FileConfigurationManager extends Disposable { isTypeScriptDocument(document) ? 'typescript.preferences' : 'javascript.preferences', document.uri); - const preferences: ExperimentalProto.UserPreferences = { + const preferences: Proto.UserPreferences = { quotePreference: this.getQuoteStylePreference(preferencesConfig), importModuleSpecifierPreference: getImportModuleSpecifierPreference(preferencesConfig), importModuleSpecifierEnding: getImportModuleSpecifierEndingPreference(preferencesConfig), - // @ts-expect-error until TS 4.5 protocol update jsxAttributeCompletionStyle: getJsxAttributeCompletionStyle(preferencesConfig), allowTextChangesInNewFiles: document.uri.scheme === fileSchemes.file, providePrefixAndSuffixTextForRename: preferencesConfig.get<boolean>('renameShorthandProperties', true) === false ? false : preferencesConfig.get<boolean>('useAliasesForRenames', true), @@ -201,6 +186,7 @@ export default class FileConfigurationManager extends Disposable { generateReturnInDocTemplate: config.get<boolean>('suggest.jsdoc.generateReturns', true), includeCompletionsForImportStatements: config.get<boolean>('suggest.includeCompletionsForImportStatements', true), includeCompletionsWithSnippetText: config.get<boolean>('suggest.includeCompletionsWithSnippetText', true), + includeCompletionsWithClassMemberSnippets: config.get<boolean>('suggest.includeCompletionsWithClassMemberSnippets', true), allowIncompleteCompletions: true, displayPartsForJSDoc: true, ...getInlayHintsPreferences(config), diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.editor.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.editor.test.ts index 3823e543c60..f2ede850c6a 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.editor.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.editor.test.ts @@ -77,7 +77,7 @@ suite.skip('Notebook Editor', function () { assert.strictEqual(editor2.viewColumn, vscode.ViewColumn.Two); }); - test.skip('Opening a notebook should fire activeNotebook event changed only once', async function () { + test('Opening a notebook should fire activeNotebook event changed only once', async function () { const openedEditor = utils.asPromise(vscode.window.onDidChangeActiveNotebookEditor); const resource = await utils.createRandomFile(undefined, undefined, '.nbdtest'); const editor = await vscode.window.showNotebookDocument(resource); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts index b89ee7b3593..165773c832a 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts @@ -190,6 +190,29 @@ suite('vscode API - window', () => { } }); + test('editor, opening multiple at the same time #134786', async () => { + const fileA = await createRandomFile(); + const fileB = await createRandomFile(); + const fileC = await createRandomFile(); + + const testFiles = [fileA, fileB, fileC]; + const result = await Promise.all(testFiles.map(async testFile => { + try { + const doc = await workspace.openTextDocument(testFile); + const editor = await window.showTextDocument(doc); + + return editor.document.uri; + } catch (error) { + return undefined; + } + })); + + assert.strictEqual(result.length, 3); + assert.strictEqual(result[0], undefined); + assert.strictEqual(result[1], undefined); + assert.strictEqual(result[2]?.toString(), fileC.toString()); + }); + test('default column when opening a file', async () => { const [docA, docB, docC] = await Promise.all([ workspace.openTextDocument(await createRandomFile()), diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts index c53015aec26..9f8da6ceb94 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts @@ -363,6 +363,70 @@ import { assertNoRpc } from '../utils'; await tasksConfig.update('tasks', []); }); }); + + test('Tasks can be run back to back', async () => { + class Pty implements Pseudoterminal { + writer = new EventEmitter<string>(); + onDidWrite = this.writer.event; + closer = new EventEmitter<number | undefined>(); + onDidClose = this.closer.event; + + constructor(readonly num: number, readonly quick: boolean) { } + + cleanup() { + this.writer.dispose(); + this.closer.dispose(); + } + + open() { + this.writer.fire('starting\r\n'); + setTimeout(() => { + this.closer.fire(this.num); + this.cleanup(); + }, this.quick ? 1 : 200); + } + + close() { + this.closer.fire(undefined); + this.cleanup(); + } + } + + async function runTask(num: number, quick: boolean) { + const pty = new Pty(num, quick); + const task = new Task( + { type: 'task_bug', exampleProp: `hello world ${num}` }, + TaskScope.Workspace, `task bug ${num}`, 'task bug', + new CustomExecution( + async () => { + return pty; + }, + )); + tasks.executeTask(task); + return new Promise<number | undefined>(resolve => { + pty.onDidClose(exitCode => { + resolve(exitCode); + }); + }); + } + + + const [r1, r2, r3, r4] = await Promise.all([ + runTask(1, false), runTask(2, false), runTask(3, false), runTask(4, false) + ]); + assert.strictEqual(r1, 1); + assert.strictEqual(r2, 2); + assert.strictEqual(r3, 3); + assert.strictEqual(r4, 4); + + const [j1, j2, j3, j4] = await Promise.all([ + runTask(5, true), runTask(6, true), runTask(7, true), runTask(8, true) + ]); + assert.strictEqual(j1, 5); + assert.strictEqual(j2, 6); + assert.strictEqual(j3, 7); + assert.strictEqual(j4, 8); + }); }); }); }); diff --git a/extensions/vscode-colorize-tests/test/colorize-results/14119_less.json b/extensions/vscode-colorize-tests/test/colorize-results/14119_less.json index ba31af17cbc..0c83af45331 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/14119_less.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/14119_less.json @@ -155,13 +155,13 @@ }, { "c": "content", - "t": "source.css.less meta.property-list.css support.type.property-name.css", + "t": "source.css.less meta.property-list.css entity.name.tag.css", "r": { - "dark_plus": "support.type.property-name: #9CDCFE", - "light_plus": "support.type.property-name: #FF0000", - "dark_vs": "support.type.property-name: #9CDCFE", - "light_vs": "support.type.property-name: #FF0000", - "hc_black": "support.type.property-name: #D4D4D4" + "dark_plus": "entity.name.tag.css: #D7BA7D", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag.css: #D7BA7D", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag.css: #D7BA7D" } }, { diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_scss.json b/extensions/vscode-colorize-tests/test/colorize-results/test_scss.json index fbe15e072a1..d78ab4d8500 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_scss.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_scss.json @@ -1299,13 +1299,13 @@ }, { "c": "font", - "t": "source.css.scss meta.property-list.scss meta.property-name.scss support.type.property-name.css", + "t": "source.css.scss meta.property-list.scss entity.name.tag.css", "r": { - "dark_plus": "support.type.property-name: #9CDCFE", - "light_plus": "support.type.property-name: #FF0000", - "dark_vs": "support.type.property-name: #9CDCFE", - "light_vs": "support.type.property-name: #FF0000", - "hc_black": "support.type.property-name: #D4D4D4" + "dark_plus": "entity.name.tag.css: #D7BA7D", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag.css: #D7BA7D", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag.css: #D7BA7D" } }, { @@ -3356,13 +3356,13 @@ }, { "c": "font", - "t": "source.css.scss meta.property-list.scss meta.property-name.scss support.type.property-name.css", + "t": "source.css.scss meta.property-list.scss entity.name.tag.css", "r": { - "dark_plus": "support.type.property-name: #9CDCFE", - "light_plus": "support.type.property-name: #FF0000", - "dark_vs": "support.type.property-name: #9CDCFE", - "light_vs": "support.type.property-name: #FF0000", - "hc_black": "support.type.property-name: #D4D4D4" + "dark_plus": "entity.name.tag.css: #D7BA7D", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag.css: #D7BA7D", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag.css: #D7BA7D" } }, { @@ -4731,13 +4731,13 @@ }, { "c": "content", - "t": "source.css.scss meta.property-list.scss meta.property-name.scss support.type.property-name.css", + "t": "source.css.scss meta.property-list.scss entity.name.tag.css", "r": { - "dark_plus": "support.type.property-name: #9CDCFE", - "light_plus": "support.type.property-name: #FF0000", - "dark_vs": "support.type.property-name: #9CDCFE", - "light_vs": "support.type.property-name: #FF0000", - "hc_black": "support.type.property-name: #D4D4D4" + "dark_plus": "entity.name.tag.css: #D7BA7D", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag.css: #D7BA7D", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag.css: #D7BA7D" } }, { @@ -12882,13 +12882,13 @@ }, { "c": "font", - "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-name.scss support.type.property-name.css", + "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss entity.name.tag.css", "r": { - "dark_plus": "support.type.property-name: #9CDCFE", - "light_plus": "support.type.property-name: #FF0000", - "dark_vs": "support.type.property-name: #9CDCFE", - "light_vs": "support.type.property-name: #FF0000", - "hc_black": "support.type.property-name: #D4D4D4" + "dark_plus": "entity.name.tag.css: #D7BA7D", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag.css: #D7BA7D", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag.css: #D7BA7D" } }, { @@ -13817,13 +13817,13 @@ }, { "c": "style", - "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-list.scss meta.property-name.scss support.type.property-name.css", + "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-list.scss entity.name.tag.css", "r": { - "dark_plus": "support.type.property-name: #9CDCFE", - "light_plus": "support.type.property-name: #FF0000", - "dark_vs": "support.type.property-name: #9CDCFE", - "light_vs": "support.type.property-name: #FF0000", - "hc_black": "support.type.property-name: #D4D4D4" + "dark_plus": "entity.name.tag.css: #D7BA7D", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag.css: #D7BA7D", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag.css: #D7BA7D" } }, { @@ -16314,13 +16314,13 @@ }, { "c": "content", - "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-name.scss support.type.property-name.css", + "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss entity.name.tag.css", "r": { - "dark_plus": "support.type.property-name: #9CDCFE", - "light_plus": "support.type.property-name: #FF0000", - "dark_vs": "support.type.property-name: #9CDCFE", - "light_vs": "support.type.property-name: #FF0000", - "hc_black": "support.type.property-name: #D4D4D4" + "dark_plus": "entity.name.tag.css: #D7BA7D", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag.css: #D7BA7D", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag.css: #D7BA7D" } }, { @@ -18019,13 +18019,13 @@ }, { "c": "a", - "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-list.scss meta.property-name.scss", + "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-list.scss entity.name.tag.css", "r": { - "dark_plus": "default: #D4D4D4", - "light_plus": "default: #000000", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", - "hc_black": "default: #FFFFFF" + "dark_plus": "entity.name.tag.css: #D7BA7D", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag.css: #D7BA7D", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag.css: #D7BA7D" } }, { @@ -18140,13 +18140,13 @@ }, { "c": "b", - "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-list.scss meta.property-name.scss", + "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-list.scss entity.name.tag.css", "r": { - "dark_plus": "default: #D4D4D4", - "light_plus": "default: #000000", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", - "hc_black": "default: #FFFFFF" + "dark_plus": "entity.name.tag.css: #D7BA7D", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag.css: #D7BA7D", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag.css: #D7BA7D" } }, { diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 30acf6771da..98f0d057758 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -24,10 +24,10 @@ fast-plist@0.1.2: resolved "https://registry.yarnpkg.com/fast-plist/-/fast-plist-0.1.2.tgz#a45aff345196006d406ca6cdcd05f69051ef35b8" integrity sha1-pFr/NFGWAG1AbKbNzQX2kFHvNbg= -typescript@4.4.3: - version "4.4.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.3.tgz#bdc5407caa2b109efd4f82fe130656f977a29324" - integrity sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA== +typescript@^4.5.0-dev.20211029: + version "4.5.0-dev.20211029" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.0-dev.20211029.tgz#ec4619ab136bd70ddd9ec1a7c18783b7ce9990a3" + integrity sha512-N+2wLMbTq+jQmad78i4wKBGcXudBFWy+QdV1Xu9cx+F5Xi6hubBotFEzS7zA7G1Eevy6NJwlsNy0G8ok2GQ9nw== vscode-grammar-updater@^1.0.3: version "1.0.3" diff --git a/package.json b/package.json index d4bec9f273e..4b3e212937b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.62.0", - "distro": "084dcc16aa74953bab0548580b3af2b0f2eb1134", + "distro": "68b7698bb44bb140dfd35df9825ea6825edf87af", "author": { "name": "Microsoft Corporation" }, @@ -63,8 +63,7 @@ "@vscode/sqlite3": "4.0.12", "@vscode/vscode-languagedetection": "1.0.21", "applicationinsights": "1.0.8", - "chokidar": "3.5.1", - "graceful-fs": "4.2.6", + "graceful-fs": "4.2.8", "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^2.2.3", "iconv-lite-umd": "0.6.8", @@ -72,7 +71,7 @@ "keytar": "7.2.0", "minimist": "^1.2.5", "native-is-elevated": "0.4.3", - "native-keymap": "3.0.0", + "native-keymap": "3.0.1", "native-watchdog": "1.3.0", "node-pty": "0.11.0-beta7", "spdlog": "^0.13.0", @@ -84,7 +83,7 @@ "vscode-proxy-agent": "^0.11.0", "vscode-regexpp": "^3.1.0", "vscode-ripgrep": "^1.12.1", - "vscode-textmate": "5.4.0", + "vscode-textmate": "5.4.1", "xterm": "4.15.0-beta.10", "xterm-addon-search": "0.9.0-beta.5", "xterm-addon-serialize": "0.7.0-beta.2", @@ -97,7 +96,6 @@ "devDependencies": { "7zip": "0.0.6", "@types/applicationinsights": "0.20.0", - "@types/chokidar": "2.1.3", "@types/cookie": "^0.3.3", "@types/copy-webpack-plugin": "^6.0.3", "@types/cssnano": "^4.0.0", @@ -199,13 +197,13 @@ "style-loader": "^1.0.0", "ts-loader": "^9.2.3", "tsec": "0.1.4", - "typescript": "^4.5.0-dev.20211021", + "typescript": "^4.5.0-dev.20211029", "typescript-formatter": "7.1.0", "underscore": "^1.12.1", "util": "^0.12.4", "vinyl": "^2.0.0", "vinyl-fs": "^3.0.0", - "vscode-debugprotocol": "1.48.0", + "vscode-debugprotocol": "1.50.0", "vscode-nls-dev": "^3.3.1", "vscode-telemetry-extractor": "^1.8.0", "webpack": "^5.42.0", diff --git a/remote/package.json b/remote/package.json index a7960f23916..1851794ddd6 100644 --- a/remote/package.json +++ b/remote/package.json @@ -7,9 +7,8 @@ "@parcel/watcher": "2.0.0", "@vscode/vscode-languagedetection": "1.0.21", "applicationinsights": "1.0.8", - "chokidar": "3.5.1", "cookie": "^0.4.0", - "graceful-fs": "4.2.6", + "graceful-fs": "4.2.8", "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^2.2.3", "iconv-lite-umd": "0.6.8", @@ -24,7 +23,7 @@ "vscode-proxy-agent": "^0.11.0", "vscode-regexpp": "^3.1.0", "vscode-ripgrep": "^1.12.1", - "vscode-textmate": "5.4.0", + "vscode-textmate": "5.4.1", "xterm": "4.15.0-beta.10", "xterm-addon-search": "0.9.0-beta.5", "xterm-addon-serialize": "0.7.0-beta.2", diff --git a/remote/web/package.json b/remote/web/package.json index 46e5dd91a02..307a919a301 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -9,7 +9,7 @@ "jschardet": "3.0.0", "tas-client-umd": "0.1.4", "vscode-oniguruma": "1.5.1", - "vscode-textmate": "5.4.0", + "vscode-textmate": "5.4.1", "xterm": "4.15.0-beta.10", "xterm-addon-search": "0.9.0-beta.5", "xterm-addon-unicode11": "0.3.0", diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 646ff9fcd3a..384abe27126 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -108,10 +108,10 @@ vscode-oniguruma@1.5.1: resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.5.1.tgz#9ca10cd3ada128bd6380344ea28844243d11f695" integrity sha512-JrBZH8DCC262TEYcYdeyZusiETu0Vli0xFgdRwNJjDcObcRjbmJP+IFcA3ScBwIXwgFHYKbAgfxtM/Cl+3Spjw== -vscode-textmate@5.4.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.4.0.tgz#4b25ffc1f14ac3a90faf9a388c67a01d24257cd7" - integrity sha512-c0Q4zYZkcLizeYJ3hNyaVUM2AA8KDhNCA3JvXY8CeZSJuBdAy3bAvSbv46RClC4P3dSO9BdwhnKEx2zOo6vP/w== +vscode-textmate@5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.4.1.tgz#09d566724fc76b60b3ad9791eebf1f0b50f29e5a" + integrity sha512-4CvPHmfuZQaXrcCpathdh6jo7myuR+MU8BvscgQADuponpbqfmu2rwTOtCXhGwwEgStvJF8V4s9FwMKRVLNmKQ== xterm-addon-search@0.9.0-beta.5: version "0.9.0-beta.5" diff --git a/remote/yarn.lock b/remote/yarn.lock index c94c62cf7da..091d7fe7dad 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -127,14 +127,6 @@ agent-base@^4.3.0: dependencies: es6-promisify "^5.0.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" - applicationinsights@1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.8.tgz#db6e3d983cf9f9405fe1ee5ba30ac6e1914537b5" @@ -144,11 +136,6 @@ applicationinsights@1.0.8: diagnostic-channel-publishers "0.2.1" zone.js "0.7.6" -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== - bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -156,33 +143,11 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -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" - buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= -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" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.5.0" - optionalDependencies: - fsevents "~2.3.1" - cookie@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" @@ -260,13 +225,6 @@ file-uri-to-path@2: resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz#7b415aeba227d575851e0a5b0c640d7656403fba" integrity sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg== -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" - fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" @@ -276,11 +234,6 @@ fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" -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== - ftp@^0.3.10: version "0.3.10" resolved "https://registry.yarnpkg.com/ftp/-/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d" @@ -301,14 +254,12 @@ get-uri@^3.0.2: fs-extra "^8.1.0" ftp "^0.3.10" -glob-parent@~5.1.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" +graceful-fs@4.2.8: + version "4.2.8" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" + integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== -graceful-fs@4.2.6, graceful-fs@^4.1.6, graceful-fs@^4.2.0: +graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.6" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== @@ -369,30 +320,6 @@ ip@^1.1.5: resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= -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-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@^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== - isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -474,26 +401,11 @@ node-pty@0.11.0-beta7: dependencies: nan "^2.14.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== - pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= -picomatch@^2.0.4: - version "2.0.7" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" - integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== - -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== - 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" @@ -509,13 +421,6 @@ readable-stream@1.1.x: isarray "0.0.1" string_decoder "~0.10.x" -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: - picomatch "^2.2.1" - semver@^5.3.0: version "5.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" @@ -562,13 +467,6 @@ tas-client-umd@0.1.4: resolved "https://registry.yarnpkg.com/tas-client-umd/-/tas-client-umd-0.1.4.tgz#49db4130dd63a8342fabf77185a740fc6a7bea80" integrity sha512-1hFqJeLD3ryNikniIaO7TItlXhS5vx7bJ+wbPDf8o+IifgwwOWK2ARisdEM9SnJd0ccfcwNPG6Po+RiKn5L2hg== -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" - universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -614,10 +512,10 @@ vscode-ripgrep@^1.12.1: https-proxy-agent "^4.0.0" proxy-from-env "^1.1.0" -vscode-textmate@5.4.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.4.0.tgz#4b25ffc1f14ac3a90faf9a388c67a01d24257cd7" - integrity sha512-c0Q4zYZkcLizeYJ3hNyaVUM2AA8KDhNCA3JvXY8CeZSJuBdAy3bAvSbv46RClC4P3dSO9BdwhnKEx2zOo6vP/w== +vscode-textmate@5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.4.1.tgz#09d566724fc76b60b3ad9791eebf1f0b50f29e5a" + integrity sha512-4CvPHmfuZQaXrcCpathdh6jo7myuR+MU8BvscgQADuponpbqfmu2rwTOtCXhGwwEgStvJF8V4s9FwMKRVLNmKQ== vscode-windows-ca-certs@^0.3.0: version "0.3.0" diff --git a/src/tsconfig.monaco.json b/src/tsconfig.monaco.json index 601aca6f9f0..5d97fdf7095 100644 --- a/src/tsconfig.monaco.json +++ b/src/tsconfig.monaco.json @@ -26,6 +26,7 @@ ], "exclude": [ "node_modules/*", - "vs/platform/files/browser/htmlFileSystemProvider.ts" + "vs/platform/files/browser/htmlFileSystemProvider.ts", + "vs/platform/assignment/*" ] } diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 6148eb30092..ac06cab08a9 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -846,6 +846,8 @@ export const EventType = { LOAD: 'load', BEFORE_UNLOAD: 'beforeunload', UNLOAD: 'unload', + PAGE_SHOW: 'pageshow', + PAGE_HIDE: 'pagehide', ABORT: 'abort', ERROR: 'error', RESIZE: 'resize', diff --git a/src/vs/base/browser/indexedDB.ts b/src/vs/base/browser/indexedDB.ts new file mode 100644 index 00000000000..7eca3955df6 --- /dev/null +++ b/src/vs/base/browser/indexedDB.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 { getErrorMessage } from 'vs/base/common/errors'; +import { mark } from 'vs/base/common/performance'; +import { isArray } from 'vs/base/common/types'; + +class MissingStoresError extends Error { + constructor(readonly db: IDBDatabase) { + super('Missing stores'); + } +} + +export class IndexedDB { + + static async create(name: string, version: number, stores: string[]): Promise<IndexedDB> { + const database = await IndexedDB.openDatabase(name, version, stores); + return new IndexedDB(database, name); + } + + static async openDatabase(name: string, version: number, stores: string[]): Promise<IDBDatabase> { + mark(`code/willOpenDatabase/${name}`); + try { + return await IndexedDB.doOpenDatabase(name, version, stores); + } catch (err) { + if (err instanceof MissingStoresError) { + console.info(`Attempting to recreate the indexedDB once.`, name); + + try { + // Try to delete the db + await IndexedDB.deleteDatabase(err.db); + } catch (error) { + console.error(`Error while deleting the indexedDB`, getErrorMessage(error)); + throw error; + } + + return await IndexedDB.doOpenDatabase(name, version, stores); + } + + throw err; + } finally { + mark(`code/didOpenDatabase/${name}`); + } + } + + private static doOpenDatabase(name: string, version: number, stores: string[]): Promise<IDBDatabase> { + return new Promise((c, e) => { + const request = window.indexedDB.open(name, version); + request.onerror = () => e(request.error); + request.onsuccess = () => { + const db = request.result; + for (const store of stores) { + if (!db.objectStoreNames.contains(store)) { + console.error(`Error while opening indexedDB. Could not find ${store} object store`); + e(new MissingStoresError(db)); + return; + } + } + c(db); + }; + request.onupgradeneeded = () => { + const db = request.result; + for (const store of stores) { + if (!db.objectStoreNames.contains(store)) { + db.createObjectStore(store); + } + } + }; + }); + } + + private static deleteDatabase(indexedDB: IDBDatabase): Promise<void> { + return new Promise((c, e) => { + // Close any opened connections + indexedDB.close(); + + // Delete the db + const deleteRequest = window.indexedDB.deleteDatabase(indexedDB.name); + deleteRequest.onerror = (err) => e(deleteRequest.error); + deleteRequest.onsuccess = () => c(); + }); + } + + private database: IDBDatabase | null = null; + private readonly pendingTransactions: IDBTransaction[] = []; + + constructor(database: IDBDatabase, private readonly name: string) { + this.database = database; + } + + getDatabase(): IDBDatabase | null { + return this.database; + } + + hasPendingTransactions(): boolean { + return this.pendingTransactions.length > 0; + } + + close(): void { + if (this.pendingTransactions.length) { + this.pendingTransactions.splice(0, this.pendingTransactions.length).forEach(transaction => transaction.abort()); + } + if (this.database) { + this.database.close(); + } + this.database = null; + } + + runInTransaction<T>(store: string, transactionMode: IDBTransactionMode, dbRequestFn: (store: IDBObjectStore) => IDBRequest<T>[]): Promise<T[]> + runInTransaction<T>(store: string, transactionMode: IDBTransactionMode, dbRequestFn: (store: IDBObjectStore) => IDBRequest<T>): Promise<T> + async runInTransaction<T>(store: string, transactionMode: IDBTransactionMode, dbRequestFn: (store: IDBObjectStore) => IDBRequest<T> | IDBRequest<T>[]): Promise<T | T[]> { + if (!this.database) { + throw new Error(`Database '${this.name}' is not opened.`); + } + const transaction = this.database.transaction([store], transactionMode); + this.pendingTransactions.push(transaction); + return new Promise<T | T[]>((c, e) => { + transaction.oncomplete = () => { + if (isArray(request)) { + c(request.map(r => r.result)); + } else { + c(request.result); + } + }; + transaction.onerror = () => e(transaction.error); + const request = dbRequestFn(transaction.objectStore(store)); + }).finally(() => this.pendingTransactions.splice(this.pendingTransactions.indexOf(transaction), 1)); + } + +} diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index b1a0a6d988d..721059b4a27 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -56,8 +56,8 @@ export interface IButtonWithDescription extends IButton { export class Button extends Disposable implements IButton { - private _element: HTMLElement; - private options: IButtonOptions; + protected _element: HTMLElement; + protected options: IButtonOptions; private buttonBackground: Color | undefined; private buttonHoverBackground: Color | undefined; @@ -307,47 +307,15 @@ export class ButtonWithDropdown extends Disposable implements IButton { } } -export class ButtonWithDescription extends Disposable implements IButtonWithDescription { +export class ButtonWithDescription extends Button implements IButtonWithDescription { - private _element: HTMLElement; private _labelElement: HTMLElement; private _descriptionElement: HTMLElement; - private options: IButtonOptions; - - private buttonBackground: Color | undefined; - private buttonHoverBackground: Color | undefined; - private buttonForeground: Color | undefined; - private buttonSecondaryBackground: Color | undefined; - private buttonSecondaryHoverBackground: Color | undefined; - private buttonSecondaryForeground: Color | undefined; - private buttonBorder: Color | undefined; - - private _onDidClick = this._register(new Emitter<Event>()); - get onDidClick(): BaseEvent<Event> { return this._onDidClick.event; } - - private focusTracker: IFocusTracker; constructor(container: HTMLElement, options?: IButtonOptions) { - super(); + super(container, options); - this.options = options || Object.create(null); - mixin(this.options, defaultOptions, false); - - this.buttonForeground = this.options.buttonForeground; - this.buttonBackground = this.options.buttonBackground; - this.buttonHoverBackground = this.options.buttonHoverBackground; - - this.buttonSecondaryForeground = this.options.buttonSecondaryForeground; - this.buttonSecondaryBackground = this.options.buttonSecondaryBackground; - this.buttonSecondaryHoverBackground = this.options.buttonSecondaryHoverBackground; - - this.buttonBorder = this.options.buttonBorder; - - this._element = document.createElement('a'); - this._element.classList.add('monaco-button'); this._element.classList.add('monaco-description-button'); - this._element.tabIndex = 0; - this._element.setAttribute('role', 'button'); this._labelElement = document.createElement('div'); this._labelElement.classList.add('monaco-button-label'); @@ -358,107 +326,9 @@ export class ButtonWithDescription extends Disposable implements IButtonWithDesc this._descriptionElement.classList.add('monaco-button-description'); this._descriptionElement.tabIndex = -1; this._element.appendChild(this._descriptionElement); - - container.appendChild(this._element); - - this._register(Gesture.addTarget(this._element)); - - [EventType.CLICK, TouchEventType.Tap].forEach(eventType => { - this._register(addDisposableListener(this._element, eventType, e => { - if (!this.enabled) { - EventHelper.stop(e); - return; - } - - this._onDidClick.fire(e); - })); - }); - - this._register(addDisposableListener(this._element, EventType.KEY_DOWN, e => { - const event = new StandardKeyboardEvent(e); - let eventHandled = false; - if (this.enabled && (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space))) { - this._onDidClick.fire(e); - eventHandled = true; - } else if (event.equals(KeyCode.Escape)) { - this._element.blur(); - eventHandled = true; - } - - if (eventHandled) { - EventHelper.stop(event, true); - } - })); - - this._register(addDisposableListener(this._element, EventType.MOUSE_OVER, e => { - if (!this._element.classList.contains('disabled')) { - this.setHoverBackground(); - } - })); - - this._register(addDisposableListener(this._element, EventType.MOUSE_OUT, e => { - this.applyStyles(); // restore standard styles - })); - - // Also set hover background when button is focused for feedback - this.focusTracker = this._register(trackFocus(this._element)); - this._register(this.focusTracker.onDidFocus(() => this.setHoverBackground())); - this._register(this.focusTracker.onDidBlur(() => this.applyStyles())); // restore standard styles - - this.applyStyles(); } - private setHoverBackground(): void { - let hoverBackground; - if (this.options.secondary) { - hoverBackground = this.buttonSecondaryHoverBackground ? this.buttonSecondaryHoverBackground.toString() : null; - } else { - hoverBackground = this.buttonHoverBackground ? this.buttonHoverBackground.toString() : null; - } - if (hoverBackground) { - this._element.style.backgroundColor = hoverBackground; - } - } - - style(styles: IButtonStyles): void { - this.buttonForeground = styles.buttonForeground; - this.buttonBackground = styles.buttonBackground; - this.buttonHoverBackground = styles.buttonHoverBackground; - this.buttonSecondaryForeground = styles.buttonSecondaryForeground; - this.buttonSecondaryBackground = styles.buttonSecondaryBackground; - this.buttonSecondaryHoverBackground = styles.buttonSecondaryHoverBackground; - this.buttonBorder = styles.buttonBorder; - - this.applyStyles(); - } - - private applyStyles(): void { - if (this._element) { - let background, foreground; - if (this.options.secondary) { - foreground = this.buttonSecondaryForeground ? this.buttonSecondaryForeground.toString() : ''; - background = this.buttonSecondaryBackground ? this.buttonSecondaryBackground.toString() : ''; - } else { - foreground = this.buttonForeground ? this.buttonForeground.toString() : ''; - background = this.buttonBackground ? this.buttonBackground.toString() : ''; - } - - const border = this.buttonBorder ? this.buttonBorder.toString() : ''; - - this._element.style.color = foreground; - this._element.style.backgroundColor = background; - - this._element.style.borderWidth = border ? '1px' : ''; - this._element.style.borderStyle = border ? 'solid' : ''; - this._element.style.borderColor = border; - } - } - - get element(): HTMLElement { - return this._element; - } - - set label(value: string) { + override set label(value: string) { this._element.classList.add('monaco-text-button'); if (this.options.supportIcons) { reset(this._labelElement, ...renderLabelWithIcons(value)); @@ -479,33 +349,6 @@ export class ButtonWithDescription extends Disposable implements IButtonWithDesc this._descriptionElement.textContent = value; } } - - set icon(icon: CSSIcon) { - this._element.classList.add(...CSSIcon.asClassNameArray(icon)); - } - - set enabled(value: boolean) { - if (value) { - this._element.classList.remove('disabled'); - this._element.setAttribute('aria-disabled', String(false)); - this._element.tabIndex = 0; - } else { - this._element.classList.add('disabled'); - this._element.setAttribute('aria-disabled', String(true)); - } - } - - get enabled() { - return !this._element.classList.contains('disabled'); - } - - focus(): void { - this._element.focus(); - } - - hasFocus(): boolean { - return this._element === document.activeElement; - } } export class ButtonBar extends Disposable { diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 38a87147102..2ea8f0ac904 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -434,7 +434,7 @@ class TypeLabelController<T> implements IDisposable { .filter(() => this.automaticKeyboardNavigation || this.triggered) .map(event => new StandardKeyboardEvent(event)) .filter(e => this.delegate.mightProducePrintableCharacter(e)) - .forEach(e => { e.stopPropagation(); e.preventDefault(); }) + .forEach(e => e.preventDefault()) .map(event => event.browserEvent.key) .event; diff --git a/src/vs/base/common/filters.ts b/src/vs/base/common/filters.ts index c491aa5d20f..e1d674ff9a3 100644 --- a/src/vs/base/common/filters.ts +++ b/src/vs/base/common/filters.ts @@ -120,7 +120,9 @@ function isWhitespace(code: number): boolean { } const wordSeparators = new Set<number>(); -'`~!@#$%^&*()-=+[{]}\\|;:\'",.<>/?' +// These are chosen as natural word separators based on writen text. +// It is a subset of the word separators used by the monaco editor. +'()[]{}<>`\'"-/;:,.?!' .split('') .forEach(s => wordSeparators.add(s.charCodeAt(0))); diff --git a/src/vs/base/common/resources.ts b/src/vs/base/common/resources.ts index 73b218c2d57..cd62839c133 100644 --- a/src/vs/base/common/resources.ts +++ b/src/vs/base/common/resources.ts @@ -276,8 +276,8 @@ export class ExtUri implements IExtUri { return !!resource.path && resource.path[0] === '/'; } - isEqualAuthority(a1: string, a2: string) { - return a1 === a2 || equalsIgnoreCase(a1, a2); + isEqualAuthority(a1: string | undefined, a2: string | undefined) { + return a1 === a2 || (a1 !== undefined && a2 !== undefined && equalsIgnoreCase(a1, a2)); } hasTrailingPathSeparator(resource: URI, sep: string = paths.sep): boolean { diff --git a/src/vs/base/node/id.ts b/src/vs/base/node/id.ts index 20b0af2e57f..8739130322f 100644 --- a/src/vs/base/node/id.ts +++ b/src/vs/base/node/id.ts @@ -93,7 +93,7 @@ export async function getMachineId(): Promise<string> { async function getMacMachineId(): Promise<string | undefined> { try { const crypto = await import('crypto'); - const macAddress = await getMac(); + const macAddress = getMac(); return crypto.createHash('sha256').update(macAddress, 'utf8').digest('hex'); } catch (err) { errors.onUnexpectedError(err); diff --git a/src/vs/base/node/macAddress.ts b/src/vs/base/node/macAddress.ts index 670bac70a5e..a593f6df3b6 100644 --- a/src/vs/base/node/macAddress.ts +++ b/src/vs/base/node/macAddress.ts @@ -16,39 +16,18 @@ function validateMacAddress(candidate: string): boolean { return !invalidMacAddresses.has(tempCandidate); } -export function getMac(): Promise<string> { - // eslint-disable-next-line no-async-promise-executor - return new Promise(async (resolve, reject) => { - const timeout = setTimeout(() => reject('Unable to retrieve mac address (timeout after 10s)'), 10000); - - try { - resolve(await doGetMac()); - } catch (error) { - reject(error); - } finally { - clearTimeout(timeout); - } - }); -} - -function doGetMac(): Promise<string> { - return new Promise((resolve, reject) => { - try { - const ifaces = networkInterfaces(); - for (let name in ifaces) { - const networkInterface = ifaces[name]; - if (networkInterface) { - for (const { mac } of networkInterface) { - if (validateMacAddress(mac)) { - return resolve(mac); - } - } +export function getMac(): string { + const ifaces = networkInterfaces(); + for (let name in ifaces) { + const networkInterface = ifaces[name]; + if (networkInterface) { + for (const { mac } of networkInterface) { + if (validateMacAddress(mac)) { + return mac; } } - - reject('Unable to retrieve mac address (unexpected format)'); - } catch (err) { - reject(err); } - }); + } + + throw new Error('Unable to retrieve mac address (unexpected format)'); } diff --git a/src/vs/base/test/browser/indexedDB.test.ts b/src/vs/base/test/browser/indexedDB.test.ts new file mode 100644 index 00000000000..e815df717af --- /dev/null +++ b/src/vs/base/test/browser/indexedDB.test.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as assert from 'assert'; +import { IndexedDB } from 'vs/base/browser/indexedDB'; + +suite('IndexedDB', () => { + + let indexedDB: IndexedDB; + + setup(async () => { + indexedDB = await IndexedDB.create('vscode-indexeddb-test', 1, ['test-store']); + await indexedDB.runInTransaction('test-store', 'readwrite', store => store.clear()); + }); + + teardown(() => { + indexedDB.close(); + }); + + test('runInTransaction', async () => { + await indexedDB.runInTransaction('test-store', 'readwrite', store => store.add('hello1', 'key1')); + const value = await indexedDB.runInTransaction('test-store', 'readonly', store => store.get('key1')); + assert.deepStrictEqual(value, 'hello1'); + }); + + test('hasPendingTransactions', async () => { + const promise = indexedDB.runInTransaction('test-store', 'readwrite', store => store.add('hello2', 'key2')); + assert.deepStrictEqual(indexedDB.hasPendingTransactions(), true); + await promise; + assert.deepStrictEqual(indexedDB.hasPendingTransactions(), false); + }); + + test('close', async () => { + const promise = indexedDB.runInTransaction('test-store', 'readwrite', store => store.add('hello3', 'key3')); + indexedDB.close(); + assert.deepStrictEqual(indexedDB.hasPendingTransactions(), false); + try { + await promise; + assert.fail('Transaction should be aborted'); + } catch (error) { } + }); + +}); diff --git a/src/vs/base/test/common/filters.test.ts b/src/vs/base/test/common/filters.test.ts index 990df495aff..0353e165cb2 100644 --- a/src/vs/base/test/common/filters.test.ts +++ b/src/vs/base/test/common/filters.test.ts @@ -200,6 +200,9 @@ suite('Filters', () => { filterOk(matchesWords, 'ƶƤk', 'Ɩhm: Ƅlles Klar', [{ start: 0, end: 1 }, { start: 5, end: 6 }, { start: 11, end: 12 }]); + // Handles issue #123915 + filterOk(matchesWords, 'C++', 'C/C++: command', [{ start: 2, end: 5 }]); + // assert.ok(matchesWords('gipu', 'Category: Git: Pull', true) === null); // assert.deepStrictEqual(matchesWords('pu', 'Category: Git: Pull', true), [{ start: 15, end: 17 }]); @@ -215,7 +218,6 @@ suite('Filters', () => { filterOk(matchesWords, 'foo bar', 'foo-bar'); filterOk(matchesWords, 'foo bar', '123 foo-bar 456'); - filterOk(matchesWords, 'foo+bar', 'foo-bar'); filterOk(matchesWords, 'foo-bar', 'foo bar'); filterOk(matchesWords, 'foo:bar', 'foo:bar'); }); diff --git a/src/vs/base/test/node/id.test.ts b/src/vs/base/test/node/id.test.ts index 4d12416329b..2ef49a3d4ba 100644 --- a/src/vs/base/test/node/id.test.ts +++ b/src/vs/base/test/node/id.test.ts @@ -16,7 +16,7 @@ flakySuite('ID', () => { }); test('getMac', async () => { - const macAddress = await getMac(); + const macAddress = getMac(); assert.ok(/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/.test(macAddress), `Expected a MAC address, got: ${macAddress}`); }); }); diff --git a/src/vs/base/test/node/processes/processes.test.ts b/src/vs/base/test/node/processes/processes.integrationTest.ts similarity index 100% rename from src/vs/base/test/node/processes/processes.test.ts rename to src/vs/base/test/node/processes/processes.integrationTest.ts diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index e8c22a0c75c..f958b760a80 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -95,6 +95,7 @@ import { SharedProcessTunnelService } from 'vs/platform/remote/node/sharedProces import { ipcSharedProcessWorkerChannelName, ISharedProcessWorkerConfiguration, ISharedProcessWorkerService } from 'vs/platform/sharedProcess/common/sharedProcessWorkerService'; import { SharedProcessWorkerService } from 'vs/platform/sharedProcess/electron-browser/sharedProcessWorkerService'; import { IUserConfigurationFileService, UserConfigurationFileServiceId } from 'vs/platform/configuration/common/userConfigurationFileService'; +import { AssignmentService } from 'vs/platform/assignment/common/assignmentService'; class SharedProcessMain extends Disposable { @@ -240,6 +241,9 @@ class SharedProcessMain extends Disposable { const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id)); services.set(IExtensionRecommendationNotificationService, new ExtensionRecommendationNotificationServiceChannelClient(this.server.getChannel('extensionRecommendationNotification', activeWindowRouter))); + // Assignment Service (Experiment service w/out scorecards) + const assignmentService = new AssignmentService(this.configuration.machineId, configurationService, productService); + // Telemetry let telemetryService: ITelemetryService; const appenders: ITelemetryAppender[] = []; @@ -250,7 +254,16 @@ class SharedProcessMain extends Disposable { // Application Insights if (productService.aiConfig && productService.aiConfig.asimovKey) { - const appInsightsAppender = new AppInsightsAppender('monacoworkbench', null, productService.aiConfig.asimovKey); + const testCollector = await assignmentService.getTreatment<boolean>('telemetryMigration') ?? false; + const insiders = productService.quality !== 'stable'; + // Insiders send to both collector and vortex if assigned. + // Stable only send to one + if (insiders && testCollector) { + const collectorAppender = new AppInsightsAppender('monacoworkbench', null, productService.aiConfig.asimovKey, testCollector, true); + this._register(toDisposable(() => collectorAppender.flush())); // Ensure the AI appender is disposed so that it flushes remaining data + appenders.push(collectorAppender); + } + const appInsightsAppender = new AppInsightsAppender('monacoworkbench', null, productService.aiConfig.asimovKey, insiders ? false : testCollector); this._register(toDisposable(() => appInsightsAppender.flush())); // Ensure the AI appender is disposed so that it flushes remaining data appenders.push(appInsightsAppender); } diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index cea03505221..323517cac2d 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -578,6 +578,9 @@ export class TextAreaHandler extends ViewPart { top, left, canUseZeroSizeTextarea ? 0 : 1, this._lineHeight ); + // In case the textarea contains a word, we're going to try to align the textarea's cursor + // with our cursor by scrolling the textarea as much as possible + this.textArea.domNode.scrollLeft = 1000000; return; } diff --git a/src/vs/editor/common/commands/replaceCommand.ts b/src/vs/editor/common/commands/replaceCommand.ts index 3e628f2db76..4d036be228c 100644 --- a/src/vs/editor/common/commands/replaceCommand.ts +++ b/src/vs/editor/common/commands/replaceCommand.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Range } from 'vs/editor/common/core/range'; -import { Selection } from 'vs/editor/common/core/selection'; +import { Selection, SelectionDirection } from 'vs/editor/common/core/selection'; import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; @@ -27,12 +27,7 @@ export class ReplaceCommand implements ICommand { public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { let inverseEditOperations = helper.getInverseEditOperations(); let srcRange = inverseEditOperations[0].range; - return new Selection( - srcRange.endLineNumber, - srcRange.endColumn, - srcRange.endLineNumber, - srcRange.endColumn - ); + return Selection.fromPositions(srcRange.getEndPosition()); } } @@ -53,7 +48,7 @@ export class ReplaceCommandThatSelectsText implements ICommand { public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { const inverseEditOperations = helper.getInverseEditOperations(); const srcRange = inverseEditOperations[0].range; - return new Selection(srcRange.startLineNumber, srcRange.startColumn, srcRange.endLineNumber, srcRange.endColumn); + return Selection.fromRange(srcRange, SelectionDirection.LTR); } } @@ -76,12 +71,7 @@ export class ReplaceCommandWithoutChangingPosition implements ICommand { public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { let inverseEditOperations = helper.getInverseEditOperations(); let srcRange = inverseEditOperations[0].range; - return new Selection( - srcRange.startLineNumber, - srcRange.startColumn, - srcRange.startLineNumber, - srcRange.startColumn - ); + return Selection.fromPositions(srcRange.getStartPosition()); } } @@ -108,12 +98,7 @@ export class ReplaceCommandWithOffsetCursorState implements ICommand { public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { let inverseEditOperations = helper.getInverseEditOperations(); let srcRange = inverseEditOperations[0].range; - return new Selection( - srcRange.endLineNumber + this._lineNumberDeltaOffset, - srcRange.endColumn + this._columnDeltaOffset, - srcRange.endLineNumber + this._lineNumberDeltaOffset, - srcRange.endColumn + this._columnDeltaOffset - ); + return Selection.fromPositions(srcRange.getEndPosition().delta(this._lineNumberDeltaOffset, this._columnDeltaOffset)); } } diff --git a/src/vs/editor/common/controller/cursorCommon.ts b/src/vs/editor/common/controller/cursorCommon.ts index f1d27757e25..b2ce0c7282b 100644 --- a/src/vs/editor/common/controller/cursorCommon.ts +++ b/src/vs/editor/common/controller/cursorCommon.ts @@ -287,31 +287,11 @@ export class SingleCursorState { } private static _computeSelection(selectionStart: Range, position: Position): Selection { - let startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number; - if (selectionStart.isEmpty()) { - startLineNumber = selectionStart.startLineNumber; - startColumn = selectionStart.startColumn; - endLineNumber = position.lineNumber; - endColumn = position.column; + if (selectionStart.isEmpty() || !position.isBeforeOrEqual(selectionStart.getStartPosition())) { + return Selection.fromPositions(selectionStart.getStartPosition(), position); } else { - if (position.isBeforeOrEqual(selectionStart.getStartPosition())) { - startLineNumber = selectionStart.endLineNumber; - startColumn = selectionStart.endColumn; - endLineNumber = position.lineNumber; - endColumn = position.column; - } else { - startLineNumber = selectionStart.startLineNumber; - startColumn = selectionStart.startColumn; - endLineNumber = position.lineNumber; - endColumn = position.column; - } + return Selection.fromPositions(selectionStart.getEndPosition(), position); } - return new Selection( - startLineNumber, - startColumn, - endLineNumber, - endColumn - ); } } @@ -365,13 +345,11 @@ export class CursorState { } public static fromModelSelection(modelSelection: ISelection): PartialModelCursorState { - const selectionStartLineNumber = modelSelection.selectionStartLineNumber; - const selectionStartColumn = modelSelection.selectionStartColumn; - const positionLineNumber = modelSelection.positionLineNumber; - const positionColumn = modelSelection.positionColumn; + const selection = Selection.liftSelection(modelSelection); const modelState = new SingleCursorState( - new Range(selectionStartLineNumber, selectionStartColumn, selectionStartLineNumber, selectionStartColumn), 0, - new Position(positionLineNumber, positionColumn), 0 + Range.fromPositions(selection.getSelectionStart()), + 0, + selection.getPosition(), 0 ); return CursorState.fromModelState(modelState); } diff --git a/src/vs/editor/common/controller/cursorTypeOperations.ts b/src/vs/editor/common/controller/cursorTypeOperations.ts index 202fb6bc0c8..09dbf3671cf 100644 --- a/src/vs/editor/common/controller/cursorTypeOperations.ts +++ b/src/vs/editor/common/controller/cursorTypeOperations.ts @@ -739,7 +739,7 @@ export class TypeOperations { if (electricAction.matchOpenBracket) { let endColumn = (lineTokens.getLineContent() + ch).lastIndexOf(electricAction.matchOpenBracket) + 1; - let match = model.findMatchingBracketUp(electricAction.matchOpenBracket, { + let match = model.bracketPairs.findMatchingBracketUp(electricAction.matchOpenBracket, { lineNumber: position.lineNumber, column: endColumn }); diff --git a/src/vs/editor/common/controller/oneCursor.ts b/src/vs/editor/common/controller/oneCursor.ts index bb07a4da717..8335e7abfec 100644 --- a/src/vs/editor/common/controller/oneCursor.ts +++ b/src/vs/editor/common/controller/oneCursor.ts @@ -6,7 +6,7 @@ import { CursorContext, CursorState, ICursorSimpleModel, SingleCursorState } from 'vs/editor/common/controller/cursorCommon'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { Selection, SelectionDirection } from 'vs/editor/common/core/selection'; +import { Selection } from 'vs/editor/common/core/selection'; import { PositionAffinity, TrackedRangeStickiness } from 'vs/editor/common/model'; /** @@ -63,10 +63,7 @@ export class Cursor { public readSelectionFromMarkers(context: CursorContext): Selection { const range = context.model._getTrackedRange(this._selTrackedRange!)!; - if (this.modelState.selection.getDirection() === SelectionDirection.LTR) { - return new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); - } - return new Selection(range.endLineNumber, range.endColumn, range.startLineNumber, range.startColumn); + return Selection.fromRange(range, this.modelState.selection.getDirection()); } public ensureValidState(context: CursorContext): void { diff --git a/src/vs/editor/common/core/selection.ts b/src/vs/editor/common/core/selection.ts index 3ee2e70af29..1e0db95c8f2 100644 --- a/src/vs/editor/common/core/selection.ts +++ b/src/vs/editor/common/core/selection.ts @@ -128,6 +128,13 @@ export class Selection extends Range { return new Position(this.positionLineNumber, this.positionColumn); } + /** + * Get the position at the start of the selection. + */ + public getSelectionStart(): Position { + return new Position(this.selectionStartLineNumber, this.selectionStartColumn); + } + /** * Create a new selection with a different `selectionStartLineNumber` and `selectionStartColumn`. */ @@ -147,6 +154,17 @@ export class Selection extends Range { return new Selection(start.lineNumber, start.column, end.lineNumber, end.column); } + /** + * Creates a `Selection` from a range, given a direction. + */ + public static fromRange(range: Range, direction: SelectionDirection): Selection { + if (direction === SelectionDirection.LTR) { + return new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); + } else { + return new Selection(range.endLineNumber, range.endColumn, range.startLineNumber, range.startColumn); + } + } + /** * Create a `Selection` from an `ISelection`. */ diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 0f12fbe999a..7266715e1f3 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -529,16 +529,6 @@ export class FindMatch { } } -/** - * @internal - */ -export interface IFoundBracket { - range: Range; - open: string[]; - close: string[]; - isOpen: boolean; -} - /** * Describes the behavior of decorations when typing/editing near their edges. * Note: Please do not edit the values, as they very carefully match `DecorationRangeBehavior` @@ -968,46 +958,6 @@ export interface ITextModel { */ getWordUntilPosition(position: IPosition): IWordAtPosition; - /** - * Find the matching bracket of `request` up, counting brackets. - * @param request The bracket we're searching for - * @param position The position at which to start the search. - * @return The range of the matching bracket, or null if the bracket match was not found. - * @internal - */ - findMatchingBracketUp(bracket: string, position: IPosition): Range | null; - - /** - * Find the first bracket in the model before `position`. - * @param position The position at which to start the search. - * @return The info for the first bracket before `position`, or null if there are no more brackets before `positions`. - * @internal - */ - findPrevBracket(position: IPosition): IFoundBracket | null; - - /** - * Find the first bracket in the model after `position`. - * @param position The position at which to start the search. - * @return The info for the first bracket after `position`, or null if there are no more brackets after `positions`. - * @internal - */ - findNextBracket(position: IPosition): IFoundBracket | null; - - /** - * Find the enclosing brackets that contain `position`. - * @param position The position at which to start the search. - * @internal - */ - findEnclosingBrackets(position: IPosition, maxDuration?: number): [Range, Range] | null; - - /** - * Given a `position`, if the position is on top or near a bracket, - * find the matching bracket of that bracket and return the ranges of both brackets. - * @param position The position at which to look for a bracket. - * @internal - */ - matchBracket(position: IPosition): [Range, Range] | null; - /** * @internal */ diff --git a/src/vs/editor/common/model/bracketPairs/bracketPairs.ts b/src/vs/editor/common/model/bracketPairs/bracketPairs.ts index d9dfc530aaf..75f5b9a3b6b 100644 --- a/src/vs/editor/common/model/bracketPairs/bracketPairs.ts +++ b/src/vs/editor/common/model/bracketPairs/bracketPairs.ts @@ -4,24 +4,70 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; -import { Range } from 'vs/editor/common/core/range'; +import { IPosition } from 'vs/editor/common/core/position'; +import { IRange, Range } from 'vs/editor/common/core/range'; export interface IBracketPairs { /** - * Gets all bracket pairs that intersect the given position. - * The result is sorted by the start position. - */ - getBracketPairsInRange(range: Range): BracketPairInfo[]; + * Is fired when bracket pairs change, either due to a text or a settings change. + */ + onDidChange: Event<void>; /** * Gets all bracket pairs that intersect the given position. * The result is sorted by the start position. */ - getBracketPairsInRangeWithMinIndentation(range: Range): BracketPairWithMinIndentationInfo[]; + getBracketPairsInRange(range: IRange): BracketPairInfo[]; - getBracketsInRange(range: Range): BracketInfo[]; + /** + * Gets all bracket pairs that intersect the given position. + * The result is sorted by the start position. + */ + getBracketPairsInRangeWithMinIndentation(range: IRange): BracketPairWithMinIndentationInfo[]; - onDidChange: Event<void>; + getBracketsInRange(range: IRange): BracketInfo[]; + + /** + * Find the matching bracket of `request` up, counting brackets. + * @param request The bracket we're searching for + * @param position The position at which to start the search. + * @return The range of the matching bracket, or null if the bracket match was not found. + */ + findMatchingBracketUp(bracket: string, position: IPosition): Range | null; + + /** + * Find the first bracket in the model before `position`. + * @param position The position at which to start the search. + * @return The info for the first bracket before `position`, or null if there are no more brackets before `positions`. + */ + findPrevBracket(position: IPosition): IFoundBracket | null; + + /** + * Find the first bracket in the model after `position`. + * @param position The position at which to start the search. + * @return The info for the first bracket after `position`, or null if there are no more brackets after `positions`. + */ + findNextBracket(position: IPosition): IFoundBracket | null; + + /** + * Find the enclosing brackets that contain `position`. + * @param position The position at which to start the search. + */ + findEnclosingBrackets(position: IPosition, maxDuration?: number): [Range, Range] | null; + + /** + * Given a `position`, if the position is on top or near a bracket, + * find the matching bracket of that bracket and return the ranges of both brackets. + * @param position The position at which to look for a bracket. + */ + matchBracket(position: IPosition): [Range, Range] | null; +} + +export interface IFoundBracket { + range: Range; + open: string[]; + close: string[]; + isOpen: boolean; } export class BracketInfo { diff --git a/src/vs/editor/common/model/bracketPairs/bracketPairsImpl.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsImpl.ts index 5d583296185..7fcf3855a54 100644 --- a/src/vs/editor/common/model/bracketPairs/bracketPairsImpl.ts +++ b/src/vs/editor/common/model/bracketPairs/bracketPairsImpl.ts @@ -5,105 +5,737 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, IReference, MutableDisposable } from 'vs/base/common/lifecycle'; +import { LineTokens } from 'vs/editor/common/core/lineTokens'; +import { IPosition, Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { ITextModel } from 'vs/editor/common/model'; -import { BracketInfo, BracketPairInfo, BracketPairWithMinIndentationInfo, IBracketPairs } from 'vs/editor/common/model/bracketPairs/bracketPairs'; -import { BackgroundTokenizationState, TextModel } from 'vs/editor/common/model/textModel'; +import { BracketPairsTree } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/bracketPairsTree'; +import { BracketInfo, BracketPairInfo, BracketPairWithMinIndentationInfo, IBracketPairs, IFoundBracket } from 'vs/editor/common/model/bracketPairs/bracketPairs'; +import { TextModel } from 'vs/editor/common/model/textModel'; import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; -import { ILanguageConfigurationService, ResolvedLanguageConfiguration } from 'vs/editor/common/modes/languageConfigurationRegistry'; -import { AstNode, AstNodeKind } from './impl/ast'; -import { TextEditInfo } from './impl/beforeEditPositionMapper'; -import { LanguageAgnosticBracketTokens } from './impl/brackets'; -import { Length, lengthAdd, lengthGreaterThanEqual, lengthLessThanEqual, lengthOfString, lengthsToRange, lengthZero, positionToLength, toLength } from './impl/length'; -import { parseDocument } from './impl/parser'; -import { DenseKeyProvider } from './impl/smallImmutableSet'; -import { FastTokenizer, TextBufferTokenizer } from './impl/tokenizer'; +import { ILanguageConfigurationService } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { ignoreBracketsInToken } from 'vs/editor/common/modes/supports'; +import { RichEditBrackets, BracketsUtils, RichEditBracket } from 'vs/editor/common/modes/supports/richEditBrackets'; export class BracketPairs extends Disposable implements IBracketPairs { - private readonly cache = this._register(new MutableDisposable<IReference<ActiveBracketPairsImpl>>()); + private readonly bracketPairsTree = this._register(new MutableDisposable<IReference<BracketPairsTree>>()); private readonly onDidChangeEmitter = new Emitter<void>(); public readonly onDidChange = this.onDidChangeEmitter.event; - get isDocumentSupported() { + private get isDocumentSupported() { const maxSupportedDocumentLength = /* max lines */ 50_000 * /* average column count */ 100; return this.textModel.getValueLength() <= maxSupportedDocumentLength; } private bracketsRequested = false; - constructor( + public constructor( private readonly textModel: TextModel, private readonly languageConfigurationService: ILanguageConfigurationService ) { super(); this._register(textModel.onDidChangeOptions(e => { - this.cache.clear(); - this.updateCache(); + this.bracketPairsTree.clear(); + this.updateBracketPairsTree(); })); this._register(textModel.onDidChangeLanguage(e => { - this.cache.clear(); - this.updateCache(); + this.bracketPairsTree.clear(); + this.updateBracketPairsTree(); })); this._register( this.languageConfigurationService.onDidChange(e => { - if (!e.languageId || this.cache.value?.object.didLanguageChange(e.languageId)) { - this.cache.clear(); - this.updateCache(); + if (!e.languageId || this.bracketPairsTree.value?.object.didLanguageChange(e.languageId)) { + this.bracketPairsTree.clear(); + this.updateBracketPairsTree(); } }) ); } - private updateCache() { + private updateBracketPairsTree() { if (this.bracketsRequested && this.isDocumentSupported) { - if (!this.cache.value) { + if (!this.bracketPairsTree.value) { const store = new DisposableStore(); - this.cache.value = createDisposableRef( + this.bracketPairsTree.value = createDisposableRef( store.add( - new ActiveBracketPairsImpl(this.textModel, (languageId) => { + new BracketPairsTree(this.textModel, (languageId) => { return this.languageConfigurationService.getLanguageConfiguration(languageId); }) ), store ); - store.add(this.cache.value.object.onDidChange(e => this.onDidChangeEmitter.fire(e))); + store.add(this.bracketPairsTree.value.object.onDidChange(e => this.onDidChangeEmitter.fire(e))); this.onDidChangeEmitter.fire(); } } else { - this.cache.clear(); + this.bracketPairsTree.clear(); this.onDidChangeEmitter.fire(); } } - handleContentChanged(change: IModelContentChangedEvent) { - this.cache.value?.object.handleContentChanged(change); + public handleContentChanged(change: IModelContentChangedEvent) { + this.bracketPairsTree.value?.object.handleContentChanged(change); } /** * Returns all bracket pairs that intersect the given range. * The result is sorted by the start position. */ - getBracketPairsInRange(range: Range): BracketPairInfo[] { + public getBracketPairsInRange(range: Range): BracketPairInfo[] { this.bracketsRequested = true; - this.updateCache(); - return this.cache.value?.object.getBracketPairsInRange(range, false) || []; + this.updateBracketPairsTree(); + return this.bracketPairsTree.value?.object.getBracketPairsInRange(range, false) || []; } - getBracketPairsInRangeWithMinIndentation(range: Range): BracketPairWithMinIndentationInfo[] { + public getBracketPairsInRangeWithMinIndentation(range: Range): BracketPairWithMinIndentationInfo[] { this.bracketsRequested = true; - this.updateCache(); - return this.cache.value?.object.getBracketPairsInRange(range, true) || []; + this.updateBracketPairsTree(); + return this.bracketPairsTree.value?.object.getBracketPairsInRange(range, true) || []; } - getBracketsInRange(range: Range): BracketInfo[] { + public getBracketsInRange(range: Range): BracketInfo[] { this.bracketsRequested = true; - this.updateCache(); - return this.cache.value?.object.getBracketsInRange(range) || []; + this.updateBracketPairsTree(); + return this.bracketPairsTree.value?.object.getBracketsInRange(range) || []; + } + + public findMatchingBracketUp(_bracket: string, _position: IPosition): Range | null { + let bracket = _bracket.toLowerCase(); + let position = this.textModel.validatePosition(_position); + + const languageId = this.textModel.getLanguageIdAtPosition(position.lineNumber, position.column); + let bracketsSupport = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets; + + if (!bracketsSupport) { + return null; + } + + let data = bracketsSupport.textIsBracket[bracket]; + + if (!data) { + return null; + } + + return stripBracketSearchCanceled(this._findMatchingBracketUp(data, position, null)); + } + + public matchBracket(position: IPosition): [Range, Range] | null { + return this._matchBracket(this.textModel.validatePosition(position)); + } + + private _establishBracketSearchOffsets(position: Position, lineTokens: LineTokens, modeBrackets: RichEditBrackets, tokenIndex: number) { + const tokenCount = lineTokens.getCount(); + const currentLanguageId = lineTokens.getLanguageId(tokenIndex); + + // limit search to not go before `maxBracketLength` + let searchStartOffset = Math.max(0, position.column - 1 - modeBrackets.maxBracketLength); + for (let i = tokenIndex - 1; i >= 0; i--) { + const tokenEndOffset = lineTokens.getEndOffset(i); + if (tokenEndOffset <= searchStartOffset) { + break; + } + if (ignoreBracketsInToken(lineTokens.getStandardTokenType(i)) || lineTokens.getLanguageId(i) !== currentLanguageId) { + searchStartOffset = tokenEndOffset; + break; + } + } + + // limit search to not go after `maxBracketLength` + let searchEndOffset = Math.min(lineTokens.getLineContent().length, position.column - 1 + modeBrackets.maxBracketLength); + for (let i = tokenIndex + 1; i < tokenCount; i++) { + const tokenStartOffset = lineTokens.getStartOffset(i); + if (tokenStartOffset >= searchEndOffset) { + break; + } + if (ignoreBracketsInToken(lineTokens.getStandardTokenType(i)) || lineTokens.getLanguageId(i) !== currentLanguageId) { + searchEndOffset = tokenStartOffset; + break; + } + } + + return { searchStartOffset, searchEndOffset }; + } + + private _matchBracket(position: Position): [Range, Range] | null { + const lineNumber = position.lineNumber; + const lineTokens = this.textModel.getLineTokens(lineNumber); + const lineText = this.textModel.getLineContent(lineNumber); + + const tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); + if (tokenIndex < 0) { + return null; + } + const currentModeBrackets = this.languageConfigurationService.getLanguageConfiguration(lineTokens.getLanguageId(tokenIndex)).brackets; + + // check that the token is not to be ignored + if (currentModeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))) { + + let { searchStartOffset, searchEndOffset } = this._establishBracketSearchOffsets(position, lineTokens, currentModeBrackets, tokenIndex); + + // it might be the case that [currentTokenStart -> currentTokenEnd] contains multiple brackets + // `bestResult` will contain the most right-side result + let bestResult: [Range, Range] | null = null; + while (true) { + const foundBracket = BracketsUtils.findNextBracketInRange(currentModeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (!foundBracket) { + // there are no more brackets in this text + break; + } + + // check that we didn't hit a bracket too far away from position + if (foundBracket.startColumn <= position.column && position.column <= foundBracket.endColumn) { + const foundBracketText = lineText.substring(foundBracket.startColumn - 1, foundBracket.endColumn - 1).toLowerCase(); + const r = this._matchFoundBracket(foundBracket, currentModeBrackets.textIsBracket[foundBracketText], currentModeBrackets.textIsOpenBracket[foundBracketText], null); + if (r) { + if (r instanceof BracketSearchCanceled) { + return null; + } + bestResult = r; + } + } + + searchStartOffset = foundBracket.endColumn - 1; + } + + if (bestResult) { + return bestResult; + } + } + + // If position is in between two tokens, try also looking in the previous token + if (tokenIndex > 0 && lineTokens.getStartOffset(tokenIndex) === position.column - 1) { + const prevTokenIndex = tokenIndex - 1; + const prevModeBrackets = this.languageConfigurationService.getLanguageConfiguration(lineTokens.getLanguageId(prevTokenIndex)).brackets; + + // check that previous token is not to be ignored + if (prevModeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(prevTokenIndex))) { + + let { searchStartOffset, searchEndOffset } = this._establishBracketSearchOffsets(position, lineTokens, prevModeBrackets, prevTokenIndex); + + const foundBracket = BracketsUtils.findPrevBracketInRange(prevModeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + + // check that we didn't hit a bracket too far away from position + if (foundBracket && foundBracket.startColumn <= position.column && position.column <= foundBracket.endColumn) { + const foundBracketText = lineText.substring(foundBracket.startColumn - 1, foundBracket.endColumn - 1).toLowerCase(); + const r = this._matchFoundBracket(foundBracket, prevModeBrackets.textIsBracket[foundBracketText], prevModeBrackets.textIsOpenBracket[foundBracketText], null); + if (r) { + if (r instanceof BracketSearchCanceled) { + return null; + } + return r; + } + } + } + } + + return null; + } + + private _matchFoundBracket(foundBracket: Range, data: RichEditBracket, isOpen: boolean, continueSearchPredicate: ContinueBracketSearchPredicate): [Range, Range] | null | BracketSearchCanceled { + if (!data) { + return null; + } + + const matched = ( + isOpen + ? this._findMatchingBracketDown(data, foundBracket.getEndPosition(), continueSearchPredicate) + : this._findMatchingBracketUp(data, foundBracket.getStartPosition(), continueSearchPredicate) + ); + + if (!matched) { + return null; + } + + if (matched instanceof BracketSearchCanceled) { + return matched; + } + + return [foundBracket, matched]; + } + + private _findMatchingBracketUp(bracket: RichEditBracket, position: Position, continueSearchPredicate: ContinueBracketSearchPredicate): Range | null | BracketSearchCanceled { + // console.log('_findMatchingBracketUp: ', 'bracket: ', JSON.stringify(bracket), 'startPosition: ', String(position)); + + const languageId = bracket.languageId; + const reversedBracketRegex = bracket.reversedRegex; + let count = -1; + + let totalCallCount = 0; + const searchPrevMatchingBracketInRange = (lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): Range | null | BracketSearchCanceled => { + while (true) { + if (continueSearchPredicate && (++totalCallCount) % 100 === 0 && !continueSearchPredicate()) { + return BracketSearchCanceled.INSTANCE; + } + const r = BracketsUtils.findPrevBracketInRange(reversedBracketRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (!r) { + break; + } + + const hitText = lineText.substring(r.startColumn - 1, r.endColumn - 1).toLowerCase(); + if (bracket.isOpen(hitText)) { + count++; + } else if (bracket.isClose(hitText)) { + count--; + } + + if (count === 0) { + return r; + } + + searchEndOffset = r.startColumn - 1; + } + + return null; + }; + + for (let lineNumber = position.lineNumber; lineNumber >= 1; lineNumber--) { + const lineTokens = this.textModel.getLineTokens(lineNumber); + const tokenCount = lineTokens.getCount(); + const lineText = this.textModel.getLineContent(lineNumber); + + let tokenIndex = tokenCount - 1; + let searchStartOffset = lineText.length; + let searchEndOffset = lineText.length; + if (lineNumber === position.lineNumber) { + tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); + searchStartOffset = position.column - 1; + searchEndOffset = position.column - 1; + } + + let prevSearchInToken = true; + for (; tokenIndex >= 0; tokenIndex--) { + const searchInToken = (lineTokens.getLanguageId(tokenIndex) === languageId && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); + + if (searchInToken) { + // this token should be searched + if (prevSearchInToken) { + // the previous token should be searched, simply extend searchStartOffset + searchStartOffset = lineTokens.getStartOffset(tokenIndex); + } else { + // the previous token should not be searched + searchStartOffset = lineTokens.getStartOffset(tokenIndex); + searchEndOffset = lineTokens.getEndOffset(tokenIndex); + } + } else { + // this token should not be searched + if (prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = searchPrevMatchingBracketInRange(lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return r; + } + } + } + + prevSearchInToken = searchInToken; + } + + if (prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = searchPrevMatchingBracketInRange(lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return r; + } + } + } + + return null; + } + + private _findMatchingBracketDown(bracket: RichEditBracket, position: Position, continueSearchPredicate: ContinueBracketSearchPredicate): Range | null | BracketSearchCanceled { + // console.log('_findMatchingBracketDown: ', 'bracket: ', JSON.stringify(bracket), 'startPosition: ', String(position)); + + const languageId = bracket.languageId; + const bracketRegex = bracket.forwardRegex; + let count = 1; + + let totalCallCount = 0; + const searchNextMatchingBracketInRange = (lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): Range | null | BracketSearchCanceled => { + while (true) { + if (continueSearchPredicate && (++totalCallCount) % 100 === 0 && !continueSearchPredicate()) { + return BracketSearchCanceled.INSTANCE; + } + const r = BracketsUtils.findNextBracketInRange(bracketRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (!r) { + break; + } + + const hitText = lineText.substring(r.startColumn - 1, r.endColumn - 1).toLowerCase(); + if (bracket.isOpen(hitText)) { + count++; + } else if (bracket.isClose(hitText)) { + count--; + } + + if (count === 0) { + return r; + } + + searchStartOffset = r.endColumn - 1; + } + + return null; + }; + + const lineCount = this.textModel.getLineCount(); + for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { + const lineTokens = this.textModel.getLineTokens(lineNumber); + const tokenCount = lineTokens.getCount(); + const lineText = this.textModel.getLineContent(lineNumber); + + let tokenIndex = 0; + let searchStartOffset = 0; + let searchEndOffset = 0; + if (lineNumber === position.lineNumber) { + tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); + searchStartOffset = position.column - 1; + searchEndOffset = position.column - 1; + } + + let prevSearchInToken = true; + for (; tokenIndex < tokenCount; tokenIndex++) { + const searchInToken = (lineTokens.getLanguageId(tokenIndex) === languageId && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); + + if (searchInToken) { + // this token should be searched + if (prevSearchInToken) { + // the previous token should be searched, simply extend searchEndOffset + searchEndOffset = lineTokens.getEndOffset(tokenIndex); + } else { + // the previous token should not be searched + searchStartOffset = lineTokens.getStartOffset(tokenIndex); + searchEndOffset = lineTokens.getEndOffset(tokenIndex); + } + } else { + // this token should not be searched + if (prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = searchNextMatchingBracketInRange(lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return r; + } + } + } + + prevSearchInToken = searchInToken; + } + + if (prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = searchNextMatchingBracketInRange(lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return r; + } + } + } + + return null; + } + + public findPrevBracket(_position: IPosition): IFoundBracket | null { + const position = this.textModel.validatePosition(_position); + + let languageId: string | null = null; + let modeBrackets: RichEditBrackets | null = null; + for (let lineNumber = position.lineNumber; lineNumber >= 1; lineNumber--) { + const lineTokens = this.textModel.getLineTokens(lineNumber); + const tokenCount = lineTokens.getCount(); + const lineText = this.textModel.getLineContent(lineNumber); + + let tokenIndex = tokenCount - 1; + let searchStartOffset = lineText.length; + let searchEndOffset = lineText.length; + if (lineNumber === position.lineNumber) { + tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); + searchStartOffset = position.column - 1; + searchEndOffset = position.column - 1; + const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); + if (languageId !== tokenLanguageId) { + languageId = tokenLanguageId; + modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets; + } + } + + let prevSearchInToken = true; + for (; tokenIndex >= 0; tokenIndex--) { + const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); + + if (languageId !== tokenLanguageId) { + // language id change! + if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = BracketsUtils.findPrevBracketInRange(modeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return this._toFoundBracket(modeBrackets, r); + } + prevSearchInToken = false; + } + languageId = tokenLanguageId; + modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets; + } + + const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); + + if (searchInToken) { + // this token should be searched + if (prevSearchInToken) { + // the previous token should be searched, simply extend searchStartOffset + searchStartOffset = lineTokens.getStartOffset(tokenIndex); + } else { + // the previous token should not be searched + searchStartOffset = lineTokens.getStartOffset(tokenIndex); + searchEndOffset = lineTokens.getEndOffset(tokenIndex); + } + } else { + // this token should not be searched + if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = BracketsUtils.findPrevBracketInRange(modeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return this._toFoundBracket(modeBrackets, r); + } + } + } + + prevSearchInToken = searchInToken; + } + + if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = BracketsUtils.findPrevBracketInRange(modeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return this._toFoundBracket(modeBrackets, r); + } + } + } + + return null; + } + + public findNextBracket(_position: IPosition): IFoundBracket | null { + const position = this.textModel.validatePosition(_position); + const lineCount = this.textModel.getLineCount(); + + let languageId: string | null = null; + let modeBrackets: RichEditBrackets | null = null; + for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { + const lineTokens = this.textModel.getLineTokens(lineNumber); + const tokenCount = lineTokens.getCount(); + const lineText = this.textModel.getLineContent(lineNumber); + + let tokenIndex = 0; + let searchStartOffset = 0; + let searchEndOffset = 0; + if (lineNumber === position.lineNumber) { + tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); + searchStartOffset = position.column - 1; + searchEndOffset = position.column - 1; + const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); + if (languageId !== tokenLanguageId) { + languageId = tokenLanguageId; + modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets; + } + } + + let prevSearchInToken = true; + for (; tokenIndex < tokenCount; tokenIndex++) { + const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); + + if (languageId !== tokenLanguageId) { + // language id change! + if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return this._toFoundBracket(modeBrackets, r); + } + prevSearchInToken = false; + } + languageId = tokenLanguageId; + modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets; + } + + const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); + if (searchInToken) { + // this token should be searched + if (prevSearchInToken) { + // the previous token should be searched, simply extend searchEndOffset + searchEndOffset = lineTokens.getEndOffset(tokenIndex); + } else { + // the previous token should not be searched + searchStartOffset = lineTokens.getStartOffset(tokenIndex); + searchEndOffset = lineTokens.getEndOffset(tokenIndex); + } + } else { + // this token should not be searched + if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return this._toFoundBracket(modeBrackets, r); + } + } + } + + prevSearchInToken = searchInToken; + } + + if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return this._toFoundBracket(modeBrackets, r); + } + } + } + + return null; + } + + public findEnclosingBrackets(_position: IPosition, maxDuration?: number): [Range, Range] | null { + let continueSearchPredicate: ContinueBracketSearchPredicate; + if (typeof maxDuration === 'undefined') { + continueSearchPredicate = null; + } else { + const startTime = Date.now(); + continueSearchPredicate = () => { + return (Date.now() - startTime <= maxDuration); + }; + } + const position = this.textModel.validatePosition(_position); + const lineCount = this.textModel.getLineCount(); + const savedCounts = new Map<string, number[]>(); + + let counts: number[] = []; + const resetCounts = (languageId: string, modeBrackets: RichEditBrackets | null) => { + if (!savedCounts.has(languageId)) { + let tmp = []; + for (let i = 0, len = modeBrackets ? modeBrackets.brackets.length : 0; i < len; i++) { + tmp[i] = 0; + } + savedCounts.set(languageId, tmp); + } + counts = savedCounts.get(languageId)!; + }; + + let totalCallCount = 0; + const searchInRange = (modeBrackets: RichEditBrackets, lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): [Range, Range] | null | BracketSearchCanceled => { + while (true) { + if (continueSearchPredicate && (++totalCallCount) % 100 === 0 && !continueSearchPredicate()) { + return BracketSearchCanceled.INSTANCE; + } + const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (!r) { + break; + } + + const hitText = lineText.substring(r.startColumn - 1, r.endColumn - 1).toLowerCase(); + const bracket = modeBrackets.textIsBracket[hitText]; + if (bracket) { + if (bracket.isOpen(hitText)) { + counts[bracket.index]++; + } else if (bracket.isClose(hitText)) { + counts[bracket.index]--; + } + + if (counts[bracket.index] === -1) { + return this._matchFoundBracket(r, bracket, false, continueSearchPredicate); + } + } + + searchStartOffset = r.endColumn - 1; + } + return null; + }; + + let languageId: string | null = null; + let modeBrackets: RichEditBrackets | null = null; + for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { + const lineTokens = this.textModel.getLineTokens(lineNumber); + const tokenCount = lineTokens.getCount(); + const lineText = this.textModel.getLineContent(lineNumber); + + let tokenIndex = 0; + let searchStartOffset = 0; + let searchEndOffset = 0; + if (lineNumber === position.lineNumber) { + tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); + searchStartOffset = position.column - 1; + searchEndOffset = position.column - 1; + const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); + if (languageId !== tokenLanguageId) { + languageId = tokenLanguageId; + modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets; + resetCounts(languageId, modeBrackets); + } + } + + let prevSearchInToken = true; + for (; tokenIndex < tokenCount; tokenIndex++) { + const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); + + if (languageId !== tokenLanguageId) { + // language id change! + if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return stripBracketSearchCanceled(r); + } + prevSearchInToken = false; + } + languageId = tokenLanguageId; + modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets; + resetCounts(languageId, modeBrackets); + } + + const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); + if (searchInToken) { + // this token should be searched + if (prevSearchInToken) { + // the previous token should be searched, simply extend searchEndOffset + searchEndOffset = lineTokens.getEndOffset(tokenIndex); + } else { + // the previous token should not be searched + searchStartOffset = lineTokens.getStartOffset(tokenIndex); + searchEndOffset = lineTokens.getEndOffset(tokenIndex); + } + } else { + // this token should not be searched + if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return stripBracketSearchCanceled(r); + } + } + } + + prevSearchInToken = searchInToken; + } + + if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return stripBracketSearchCanceled(r); + } + } + } + + return null; + } + + private _toFoundBracket(modeBrackets: RichEditBrackets, r: Range): IFoundBracket | null { + if (!r) { + return null; + } + + let text = this.textModel.getValueInRange(r); + text = text.toLowerCase(); + + let data = modeBrackets.textIsBracket[text]; + if (!data) { + return null; + } + + return { + range: r, + open: data.open, + close: data.close, + isOpen: modeBrackets.textIsOpenBracket[text] + }; } } @@ -114,213 +746,17 @@ function createDisposableRef<T>(object: T, disposable?: IDisposable): IReference }; } -class ActiveBracketPairsImpl extends Disposable { - private readonly didChangeEmitter = new Emitter<void>(); +type ContinueBracketSearchPredicate = null | (() => boolean); - /* - There are two trees: - * The initial tree that has no token information and is used for performant initial bracket colorization. - * The tree that used token information to detect bracket pairs. - - To prevent flickering, we only switch from the initial tree to tree with token information - when tokenization completes. - Since the text can be edited while background tokenization is in progress, we need to update both trees. - */ - private initialAstWithoutTokens: AstNode | undefined; - private astWithTokens: AstNode | undefined; - - private readonly denseKeyProvider = new DenseKeyProvider<string>(); - private readonly brackets = new LanguageAgnosticBracketTokens(this.denseKeyProvider, this.getLanguageConfiguration); - - public didLanguageChange(languageId: string): boolean { - return this.brackets.didLanguageChange(languageId); - } - - public readonly onDidChange = this.didChangeEmitter.event; - - public constructor( - private readonly textModel: TextModel, - private readonly getLanguageConfiguration: (languageId: string) => ResolvedLanguageConfiguration - ) { - super(); - - this._register(textModel.onBackgroundTokenizationStateChanged(() => { - if (textModel.backgroundTokenizationState === BackgroundTokenizationState.Completed) { - const wasUndefined = this.initialAstWithoutTokens === undefined; - // Clear the initial tree as we can use the tree with token information now. - this.initialAstWithoutTokens = undefined; - if (!wasUndefined) { - this.didChangeEmitter.fire(); - } - } - })); - - this._register(textModel.onDidChangeTokens(({ ranges }) => { - const edits = ranges.map(r => - new TextEditInfo( - toLength(r.fromLineNumber - 1, 0), - toLength(r.toLineNumber, 0), - toLength(r.toLineNumber - r.fromLineNumber + 1, 0) - ) - ); - this.astWithTokens = this.parseDocumentFromTextBuffer(edits, this.astWithTokens, false); - if (!this.initialAstWithoutTokens) { - this.didChangeEmitter.fire(); - } - })); - - if (textModel.backgroundTokenizationState === BackgroundTokenizationState.Uninitialized) { - // There are no token information yet - const brackets = this.brackets.getSingleLanguageBracketTokens(this.textModel.getLanguageId()); - const tokenizer = new FastTokenizer(this.textModel.getValue(), brackets); - this.initialAstWithoutTokens = parseDocument(tokenizer, [], undefined, true); - this.astWithTokens = this.initialAstWithoutTokens; - } else if (textModel.backgroundTokenizationState === BackgroundTokenizationState.Completed) { - // Skip the initial ast, as there is no flickering. - // Directly create the tree with token information. - this.initialAstWithoutTokens = undefined; - this.astWithTokens = this.parseDocumentFromTextBuffer([], undefined, false); - } else if (textModel.backgroundTokenizationState === BackgroundTokenizationState.InProgress) { - this.initialAstWithoutTokens = this.parseDocumentFromTextBuffer([], undefined, true); - this.astWithTokens = this.initialAstWithoutTokens; - } - } - - public handleContentChanged(change: IModelContentChangedEvent) { - const edits = change.changes.map(c => { - const range = Range.lift(c.range); - return new TextEditInfo( - positionToLength(range.getStartPosition()), - positionToLength(range.getEndPosition()), - lengthOfString(c.text) - ); - }).reverse(); - - this.astWithTokens = this.parseDocumentFromTextBuffer(edits, this.astWithTokens, false); - if (this.initialAstWithoutTokens) { - this.initialAstWithoutTokens = this.parseDocumentFromTextBuffer(edits, this.initialAstWithoutTokens, false); - } - } - - /** - * @pure (only if isPure = true) - */ - private parseDocumentFromTextBuffer(edits: TextEditInfo[], previousAst: AstNode | undefined, immutable: boolean): AstNode { - // Is much faster if `isPure = false`. - const isPure = false; - const previousAstClone = isPure ? previousAst?.deepClone() : previousAst; - const tokenizer = new TextBufferTokenizer(this.textModel, this.brackets); - const result = parseDocument(tokenizer, edits, previousAstClone, immutable); - return result; - } - - public getBracketsInRange(range: Range): BracketInfo[] { - const startOffset = toLength(range.startLineNumber - 1, range.startColumn - 1); - const endOffset = toLength(range.endLineNumber - 1, range.endColumn - 1); - const result = new Array<BracketInfo>(); - const node = this.initialAstWithoutTokens || this.astWithTokens!; - collectBrackets(node, lengthZero, node.length, startOffset, endOffset, result); - return result; - } - - public getBracketPairsInRange(range: Range, includeMinIndentation: boolean): BracketPairWithMinIndentationInfo[] { - const result = new Array<BracketPairWithMinIndentationInfo>(); - - const startLength = positionToLength(range.getStartPosition()); - const endLength = positionToLength(range.getEndPosition()); - - const node = this.initialAstWithoutTokens || this.astWithTokens!; - const context = new CollectBracketPairsContext(result, includeMinIndentation, this.textModel); - collectBracketPairs(node, lengthZero, node.length, startLength, endLength, context); - - return result; - } +class BracketSearchCanceled { + public static INSTANCE = new BracketSearchCanceled(); + _searchCanceledBrand = undefined; + private constructor() { } } -function collectBrackets(node: AstNode, nodeOffsetStart: Length, nodeOffsetEnd: Length, startOffset: Length, endOffset: Length, result: BracketInfo[], level: number = 0): void { - if (node.kind === AstNodeKind.Bracket) { - const range = lengthsToRange(nodeOffsetStart, nodeOffsetEnd); - result.push(new BracketInfo(range, level - 1, false)); - } else if (node.kind === AstNodeKind.UnexpectedClosingBracket) { - const range = lengthsToRange(nodeOffsetStart, nodeOffsetEnd); - result.push(new BracketInfo(range, level - 1, true)); - } else if (node.kind === AstNodeKind.List) { - for (const child of node.children) { - nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length); - if (lengthLessThanEqual(nodeOffsetStart, endOffset) && lengthGreaterThanEqual(nodeOffsetEnd, startOffset)) { - collectBrackets(child, nodeOffsetStart, nodeOffsetEnd, startOffset, endOffset, result, level); - } - nodeOffsetStart = nodeOffsetEnd; - } - } else if (node.kind === AstNodeKind.Pair) { - // Don't use node.children here to improve performance - level++; - - { - const child = node.openingBracket; - nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length); - if (lengthLessThanEqual(nodeOffsetStart, endOffset) && lengthGreaterThanEqual(nodeOffsetEnd, startOffset)) { - collectBrackets(child, nodeOffsetStart, nodeOffsetEnd, startOffset, endOffset, result, level); - } - nodeOffsetStart = nodeOffsetEnd; - } - - if (node.child) { - const child = node.child; - nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length); - if (lengthLessThanEqual(nodeOffsetStart, endOffset) && lengthGreaterThanEqual(nodeOffsetEnd, startOffset)) { - collectBrackets(child, nodeOffsetStart, nodeOffsetEnd, startOffset, endOffset, result, level); - } - nodeOffsetStart = nodeOffsetEnd; - } - if (node.closingBracket) { - const child = node.closingBracket; - nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length); - if (lengthLessThanEqual(nodeOffsetStart, endOffset) && lengthGreaterThanEqual(nodeOffsetEnd, startOffset)) { - collectBrackets(child, nodeOffsetStart, nodeOffsetEnd, startOffset, endOffset, result, level); - } - nodeOffsetStart = nodeOffsetEnd; - } +function stripBracketSearchCanceled<T>(result: T | null | BracketSearchCanceled): T | null { + if (result instanceof BracketSearchCanceled) { + return null; } + return result; } - -class CollectBracketPairsContext { - constructor( - public readonly result: BracketPairWithMinIndentationInfo[], - public readonly includeMinIndentation: boolean, - public readonly textModel: ITextModel, - ) { - } -} - -function collectBracketPairs(node: AstNode, nodeOffset: Length, nodeOffsetEnd: Length, startOffset: Length, endOffset: Length, context: CollectBracketPairsContext, level: number = 0) { - if (node.kind === AstNodeKind.Pair) { - const openingBracketEnd = lengthAdd(nodeOffset, node.openingBracket.length); - let minIndentation = -1; - if (context.includeMinIndentation) { - minIndentation = node.computeMinIndentation(nodeOffset, context.textModel); - } - - context.result.push(new BracketPairWithMinIndentationInfo( - lengthsToRange(nodeOffset, nodeOffsetEnd), - lengthsToRange(nodeOffset, openingBracketEnd), - node.closingBracket - ? lengthsToRange(lengthAdd(openingBracketEnd, node.child?.length || lengthZero), nodeOffsetEnd) - : undefined, - level, - minIndentation - )); - level++; - } - - let curOffset = nodeOffset; - for (const child of node.children) { - const childOffset = curOffset; - curOffset = lengthAdd(curOffset, child.length); - - if (lengthLessThanEqual(childOffset, endOffset) && lengthLessThanEqual(startOffset, curOffset)) { - collectBracketPairs(child, childOffset, curOffset, startOffset, endOffset, context, level); - } - } -} - diff --git a/src/vs/editor/common/model/bracketPairs/impl/ast.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/ast.ts similarity index 100% rename from src/vs/editor/common/model/bracketPairs/impl/ast.ts rename to src/vs/editor/common/model/bracketPairs/bracketPairsTree/ast.ts diff --git a/src/vs/editor/common/model/bracketPairs/impl/beforeEditPositionMapper.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/beforeEditPositionMapper.ts similarity index 100% rename from src/vs/editor/common/model/bracketPairs/impl/beforeEditPositionMapper.ts rename to src/vs/editor/common/model/bracketPairs/bracketPairsTree/beforeEditPositionMapper.ts diff --git a/src/vs/editor/common/model/bracketPairs/bracketPairsTree/bracketPairsTree.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/bracketPairsTree.ts new file mode 100644 index 00000000000..450d570e6fe --- /dev/null +++ b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/bracketPairsTree.ts @@ -0,0 +1,231 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; +import { BracketInfo, BracketPairWithMinIndentationInfo } from 'vs/editor/common/model/bracketPairs/bracketPairs'; +import { BackgroundTokenizationState, TextModel } from 'vs/editor/common/model/textModel'; +import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; +import { ResolvedLanguageConfiguration } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { AstNode, AstNodeKind } from './ast'; +import { TextEditInfo } from './beforeEditPositionMapper'; +import { LanguageAgnosticBracketTokens } from './brackets'; +import { Length, lengthAdd, lengthGreaterThanEqual, lengthLessThanEqual, lengthOfString, lengthsToRange, lengthZero, positionToLength, toLength } from './length'; +import { parseDocument } from './parser'; +import { DenseKeyProvider } from './smallImmutableSet'; +import { FastTokenizer, TextBufferTokenizer } from './tokenizer'; + +export class BracketPairsTree extends Disposable { + private readonly didChangeEmitter = new Emitter<void>(); + + /* + There are two trees: + * The initial tree that has no token information and is used for performant initial bracket colorization. + * The tree that used token information to detect bracket pairs. + + To prevent flickering, we only switch from the initial tree to tree with token information + when tokenization completes. + Since the text can be edited while background tokenization is in progress, we need to update both trees. + */ + private initialAstWithoutTokens: AstNode | undefined; + private astWithTokens: AstNode | undefined; + + private readonly denseKeyProvider = new DenseKeyProvider<string>(); + private readonly brackets = new LanguageAgnosticBracketTokens(this.denseKeyProvider, this.getLanguageConfiguration); + + public didLanguageChange(languageId: string): boolean { + return this.brackets.didLanguageChange(languageId); + } + + public readonly onDidChange = this.didChangeEmitter.event; + + public constructor( + private readonly textModel: TextModel, + private readonly getLanguageConfiguration: (languageId: string) => ResolvedLanguageConfiguration + ) { + super(); + + this._register(textModel.onBackgroundTokenizationStateChanged(() => { + if (textModel.backgroundTokenizationState === BackgroundTokenizationState.Completed) { + const wasUndefined = this.initialAstWithoutTokens === undefined; + // Clear the initial tree as we can use the tree with token information now. + this.initialAstWithoutTokens = undefined; + if (!wasUndefined) { + this.didChangeEmitter.fire(); + } + } + })); + + this._register(textModel.onDidChangeTokens(({ ranges }) => { + const edits = ranges.map(r => + new TextEditInfo( + toLength(r.fromLineNumber - 1, 0), + toLength(r.toLineNumber, 0), + toLength(r.toLineNumber - r.fromLineNumber + 1, 0) + ) + ); + this.astWithTokens = this.parseDocumentFromTextBuffer(edits, this.astWithTokens, false); + if (!this.initialAstWithoutTokens) { + this.didChangeEmitter.fire(); + } + })); + + if (textModel.backgroundTokenizationState === BackgroundTokenizationState.Uninitialized) { + // There are no token information yet + const brackets = this.brackets.getSingleLanguageBracketTokens(this.textModel.getLanguageId()); + const tokenizer = new FastTokenizer(this.textModel.getValue(), brackets); + this.initialAstWithoutTokens = parseDocument(tokenizer, [], undefined, true); + this.astWithTokens = this.initialAstWithoutTokens; + } else if (textModel.backgroundTokenizationState === BackgroundTokenizationState.Completed) { + // Skip the initial ast, as there is no flickering. + // Directly create the tree with token information. + this.initialAstWithoutTokens = undefined; + this.astWithTokens = this.parseDocumentFromTextBuffer([], undefined, false); + } else if (textModel.backgroundTokenizationState === BackgroundTokenizationState.InProgress) { + this.initialAstWithoutTokens = this.parseDocumentFromTextBuffer([], undefined, true); + this.astWithTokens = this.initialAstWithoutTokens; + } + } + + public handleContentChanged(change: IModelContentChangedEvent) { + const edits = change.changes.map(c => { + const range = Range.lift(c.range); + return new TextEditInfo( + positionToLength(range.getStartPosition()), + positionToLength(range.getEndPosition()), + lengthOfString(c.text) + ); + }).reverse(); + + this.astWithTokens = this.parseDocumentFromTextBuffer(edits, this.astWithTokens, false); + if (this.initialAstWithoutTokens) { + this.initialAstWithoutTokens = this.parseDocumentFromTextBuffer(edits, this.initialAstWithoutTokens, false); + } + } + + /** + * @pure (only if isPure = true) + */ + private parseDocumentFromTextBuffer(edits: TextEditInfo[], previousAst: AstNode | undefined, immutable: boolean): AstNode { + // Is much faster if `isPure = false`. + const isPure = false; + const previousAstClone = isPure ? previousAst?.deepClone() : previousAst; + const tokenizer = new TextBufferTokenizer(this.textModel, this.brackets); + const result = parseDocument(tokenizer, edits, previousAstClone, immutable); + return result; + } + + public getBracketsInRange(range: Range): BracketInfo[] { + const startOffset = toLength(range.startLineNumber - 1, range.startColumn - 1); + const endOffset = toLength(range.endLineNumber - 1, range.endColumn - 1); + const result = new Array<BracketInfo>(); + const node = this.initialAstWithoutTokens || this.astWithTokens!; + collectBrackets(node, lengthZero, node.length, startOffset, endOffset, result); + return result; + } + + public getBracketPairsInRange(range: Range, includeMinIndentation: boolean): BracketPairWithMinIndentationInfo[] { + const result = new Array<BracketPairWithMinIndentationInfo>(); + + const startLength = positionToLength(range.getStartPosition()); + const endLength = positionToLength(range.getEndPosition()); + + const node = this.initialAstWithoutTokens || this.astWithTokens!; + const context = new CollectBracketPairsContext(result, includeMinIndentation, this.textModel); + collectBracketPairs(node, lengthZero, node.length, startLength, endLength, context); + + return result; + } +} + +function collectBrackets(node: AstNode, nodeOffsetStart: Length, nodeOffsetEnd: Length, startOffset: Length, endOffset: Length, result: BracketInfo[], level: number = 0): void { + if (node.kind === AstNodeKind.Bracket) { + const range = lengthsToRange(nodeOffsetStart, nodeOffsetEnd); + result.push(new BracketInfo(range, level - 1, false)); + } else if (node.kind === AstNodeKind.UnexpectedClosingBracket) { + const range = lengthsToRange(nodeOffsetStart, nodeOffsetEnd); + result.push(new BracketInfo(range, level - 1, true)); + } else if (node.kind === AstNodeKind.List) { + for (const child of node.children) { + nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length); + if (lengthLessThanEqual(nodeOffsetStart, endOffset) && lengthGreaterThanEqual(nodeOffsetEnd, startOffset)) { + collectBrackets(child, nodeOffsetStart, nodeOffsetEnd, startOffset, endOffset, result, level); + } + nodeOffsetStart = nodeOffsetEnd; + } + } else if (node.kind === AstNodeKind.Pair) { + // Don't use node.children here to improve performance + level++; + + { + const child = node.openingBracket; + nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length); + if (lengthLessThanEqual(nodeOffsetStart, endOffset) && lengthGreaterThanEqual(nodeOffsetEnd, startOffset)) { + collectBrackets(child, nodeOffsetStart, nodeOffsetEnd, startOffset, endOffset, result, level); + } + nodeOffsetStart = nodeOffsetEnd; + } + + if (node.child) { + const child = node.child; + nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length); + if (lengthLessThanEqual(nodeOffsetStart, endOffset) && lengthGreaterThanEqual(nodeOffsetEnd, startOffset)) { + collectBrackets(child, nodeOffsetStart, nodeOffsetEnd, startOffset, endOffset, result, level); + } + nodeOffsetStart = nodeOffsetEnd; + } + if (node.closingBracket) { + const child = node.closingBracket; + nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length); + if (lengthLessThanEqual(nodeOffsetStart, endOffset) && lengthGreaterThanEqual(nodeOffsetEnd, startOffset)) { + collectBrackets(child, nodeOffsetStart, nodeOffsetEnd, startOffset, endOffset, result, level); + } + nodeOffsetStart = nodeOffsetEnd; + } + } +} + +class CollectBracketPairsContext { + constructor( + public readonly result: BracketPairWithMinIndentationInfo[], + public readonly includeMinIndentation: boolean, + public readonly textModel: ITextModel, + ) { + } +} + +function collectBracketPairs(node: AstNode, nodeOffset: Length, nodeOffsetEnd: Length, startOffset: Length, endOffset: Length, context: CollectBracketPairsContext, level: number = 0) { + if (node.kind === AstNodeKind.Pair) { + const openingBracketEnd = lengthAdd(nodeOffset, node.openingBracket.length); + let minIndentation = -1; + if (context.includeMinIndentation) { + minIndentation = node.computeMinIndentation(nodeOffset, context.textModel); + } + + context.result.push(new BracketPairWithMinIndentationInfo( + lengthsToRange(nodeOffset, nodeOffsetEnd), + lengthsToRange(nodeOffset, openingBracketEnd), + node.closingBracket + ? lengthsToRange(lengthAdd(openingBracketEnd, node.child?.length || lengthZero), nodeOffsetEnd) + : undefined, + level, + minIndentation + )); + level++; + } + + let curOffset = nodeOffset; + for (const child of node.children) { + const childOffset = curOffset; + curOffset = lengthAdd(curOffset, child.length); + + if (lengthLessThanEqual(childOffset, endOffset) && lengthLessThanEqual(startOffset, curOffset)) { + collectBracketPairs(child, childOffset, curOffset, startOffset, endOffset, context, level); + } + } +} + diff --git a/src/vs/editor/common/model/bracketPairs/impl/brackets.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/brackets.ts similarity index 100% rename from src/vs/editor/common/model/bracketPairs/impl/brackets.ts rename to src/vs/editor/common/model/bracketPairs/bracketPairsTree/brackets.ts diff --git a/src/vs/editor/common/model/bracketPairs/impl/concat23Trees.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/concat23Trees.ts similarity index 100% rename from src/vs/editor/common/model/bracketPairs/impl/concat23Trees.ts rename to src/vs/editor/common/model/bracketPairs/bracketPairsTree/concat23Trees.ts diff --git a/src/vs/editor/common/model/bracketPairs/impl/length.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/length.ts similarity index 100% rename from src/vs/editor/common/model/bracketPairs/impl/length.ts rename to src/vs/editor/common/model/bracketPairs/bracketPairsTree/length.ts diff --git a/src/vs/editor/common/model/bracketPairs/impl/nodeReader.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/nodeReader.ts similarity index 100% rename from src/vs/editor/common/model/bracketPairs/impl/nodeReader.ts rename to src/vs/editor/common/model/bracketPairs/bracketPairsTree/nodeReader.ts diff --git a/src/vs/editor/common/model/bracketPairs/impl/parser.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/parser.ts similarity index 100% rename from src/vs/editor/common/model/bracketPairs/impl/parser.ts rename to src/vs/editor/common/model/bracketPairs/bracketPairsTree/parser.ts diff --git a/src/vs/editor/common/model/bracketPairs/impl/smallImmutableSet.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/smallImmutableSet.ts similarity index 100% rename from src/vs/editor/common/model/bracketPairs/impl/smallImmutableSet.ts rename to src/vs/editor/common/model/bracketPairs/bracketPairsTree/smallImmutableSet.ts diff --git a/src/vs/editor/common/model/bracketPairs/impl/tokenizer.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/tokenizer.ts similarity index 100% rename from src/vs/editor/common/model/bracketPairs/impl/tokenizer.ts rename to src/vs/editor/common/model/bracketPairs/bracketPairsTree/tokenizer.ts diff --git a/src/vs/editor/common/model/mirrorTextModel.ts b/src/vs/editor/common/model/mirrorTextModel.ts index 4fefa551a1a..9ff680f395f 100644 --- a/src/vs/editor/common/model/mirrorTextModel.ts +++ b/src/vs/editor/common/model/mirrorTextModel.ts @@ -106,7 +106,7 @@ export class MirrorTextModel implements IMirrorTextModel { this._lines[lineIndex] = newValue; if (this._lineStarts) { // update prefix sum - this._lineStarts.changeValue(lineIndex, this._lines[lineIndex].length + this._eol.length); + this._lineStarts.setValue(lineIndex, this._lines[lineIndex].length + this._eol.length); } } diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 51b7208cdb8..828352da600 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -27,8 +27,6 @@ import { getWordAtText } from 'vs/editor/common/model/wordHelper'; import { FormattingOptions } from 'vs/editor/common/modes'; import { ILanguageConfigurationService, ResolvedLanguageConfiguration } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { NULL_MODE_ID } from 'vs/editor/common/modes/nullMode'; -import { ignoreBracketsInToken } from 'vs/editor/common/modes/supports'; -import { BracketsUtils, RichEditBracket, RichEditBrackets } from 'vs/editor/common/modes/supports/richEditBrackets'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { VSBufferReadableStream, VSBuffer } from 'vs/base/common/buffer'; import { TokensStore, MultilineTokens, countEOL, MultilineTokens2, TokensStore2 } from 'vs/editor/common/model/tokensStore'; @@ -174,21 +172,6 @@ export const enum BackgroundTokenizationState { Completed = 2, } -type ContinueBracketSearchPredicate = null | (() => boolean); - -class BracketSearchCanceled { - public static INSTANCE = new BracketSearchCanceled(); - _searchCanceledBrand = undefined; - private constructor() { } -} - -function stripBracketSearchCanceled<T>(result: T | null | BracketSearchCanceled): T | null { - if (result instanceof BracketSearchCanceled) { - return null; - } - return result; -} - export class TextModel extends Disposable implements model.ITextModel, IDecorationsTreesHost { private static readonly MODEL_SYNC_LIMIT = 50 * 1024 * 1024; // 50 MB @@ -2222,642 +2205,6 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati }; } - public findMatchingBracketUp(_bracket: string, _position: IPosition): Range | null { - let bracket = _bracket.toLowerCase(); - let position = this.validatePosition(_position); - - let lineTokens = this._getLineTokens(position.lineNumber); - let languageId = lineTokens.getLanguageId(lineTokens.findTokenIndexAtOffset(position.column - 1)); - let bracketsSupport = this.getLanguageConfiguration(languageId).brackets; - - if (!bracketsSupport) { - return null; - } - - let data = bracketsSupport.textIsBracket[bracket]; - - if (!data) { - return null; - } - - return stripBracketSearchCanceled(this._findMatchingBracketUp(data, position, null)); - } - - public matchBracket(position: IPosition): [Range, Range] | null { - return this._matchBracket(this.validatePosition(position)); - } - - private _establishBracketSearchOffsets(position: Position, lineTokens: LineTokens, modeBrackets: RichEditBrackets, tokenIndex: number) { - const tokenCount = lineTokens.getCount(); - const currentLanguageId = lineTokens.getLanguageId(tokenIndex); - - // limit search to not go before `maxBracketLength` - let searchStartOffset = Math.max(0, position.column - 1 - modeBrackets.maxBracketLength); - for (let i = tokenIndex - 1; i >= 0; i--) { - const tokenEndOffset = lineTokens.getEndOffset(i); - if (tokenEndOffset <= searchStartOffset) { - break; - } - if (ignoreBracketsInToken(lineTokens.getStandardTokenType(i)) || lineTokens.getLanguageId(i) !== currentLanguageId) { - searchStartOffset = tokenEndOffset; - break; - } - } - - // limit search to not go after `maxBracketLength` - let searchEndOffset = Math.min(lineTokens.getLineContent().length, position.column - 1 + modeBrackets.maxBracketLength); - for (let i = tokenIndex + 1; i < tokenCount; i++) { - const tokenStartOffset = lineTokens.getStartOffset(i); - if (tokenStartOffset >= searchEndOffset) { - break; - } - if (ignoreBracketsInToken(lineTokens.getStandardTokenType(i)) || lineTokens.getLanguageId(i) !== currentLanguageId) { - searchEndOffset = tokenStartOffset; - break; - } - } - - return { searchStartOffset, searchEndOffset }; - } - - private _matchBracket(position: Position): [Range, Range] | null { - const lineNumber = position.lineNumber; - const lineTokens = this._getLineTokens(lineNumber); - const lineText = this._buffer.getLineContent(lineNumber); - - const tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); - if (tokenIndex < 0) { - return null; - } - const currentModeBrackets = this.getLanguageConfiguration(lineTokens.getLanguageId(tokenIndex)).brackets; - - // check that the token is not to be ignored - if (currentModeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))) { - - let { searchStartOffset, searchEndOffset } = this._establishBracketSearchOffsets(position, lineTokens, currentModeBrackets, tokenIndex); - - // it might be the case that [currentTokenStart -> currentTokenEnd] contains multiple brackets - // `bestResult` will contain the most right-side result - let bestResult: [Range, Range] | null = null; - while (true) { - const foundBracket = BracketsUtils.findNextBracketInRange(currentModeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (!foundBracket) { - // there are no more brackets in this text - break; - } - - // check that we didn't hit a bracket too far away from position - if (foundBracket.startColumn <= position.column && position.column <= foundBracket.endColumn) { - const foundBracketText = lineText.substring(foundBracket.startColumn - 1, foundBracket.endColumn - 1).toLowerCase(); - const r = this._matchFoundBracket(foundBracket, currentModeBrackets.textIsBracket[foundBracketText], currentModeBrackets.textIsOpenBracket[foundBracketText], null); - if (r) { - if (r instanceof BracketSearchCanceled) { - return null; - } - bestResult = r; - } - } - - searchStartOffset = foundBracket.endColumn - 1; - } - - if (bestResult) { - return bestResult; - } - } - - // If position is in between two tokens, try also looking in the previous token - if (tokenIndex > 0 && lineTokens.getStartOffset(tokenIndex) === position.column - 1) { - const prevTokenIndex = tokenIndex - 1; - const prevModeBrackets = this.getLanguageConfiguration(lineTokens.getLanguageId(prevTokenIndex)).brackets; - - // check that previous token is not to be ignored - if (prevModeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(prevTokenIndex))) { - - let { searchStartOffset, searchEndOffset } = this._establishBracketSearchOffsets(position, lineTokens, prevModeBrackets, prevTokenIndex); - - const foundBracket = BracketsUtils.findPrevBracketInRange(prevModeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - - // check that we didn't hit a bracket too far away from position - if (foundBracket && foundBracket.startColumn <= position.column && position.column <= foundBracket.endColumn) { - const foundBracketText = lineText.substring(foundBracket.startColumn - 1, foundBracket.endColumn - 1).toLowerCase(); - const r = this._matchFoundBracket(foundBracket, prevModeBrackets.textIsBracket[foundBracketText], prevModeBrackets.textIsOpenBracket[foundBracketText], null); - if (r) { - if (r instanceof BracketSearchCanceled) { - return null; - } - return r; - } - } - } - } - - return null; - } - - private _matchFoundBracket(foundBracket: Range, data: RichEditBracket, isOpen: boolean, continueSearchPredicate: ContinueBracketSearchPredicate): [Range, Range] | null | BracketSearchCanceled { - if (!data) { - return null; - } - - const matched = ( - isOpen - ? this._findMatchingBracketDown(data, foundBracket.getEndPosition(), continueSearchPredicate) - : this._findMatchingBracketUp(data, foundBracket.getStartPosition(), continueSearchPredicate) - ); - - if (!matched) { - return null; - } - - if (matched instanceof BracketSearchCanceled) { - return matched; - } - - return [foundBracket, matched]; - } - - private _findMatchingBracketUp(bracket: RichEditBracket, position: Position, continueSearchPredicate: ContinueBracketSearchPredicate): Range | null | BracketSearchCanceled { - // console.log('_findMatchingBracketUp: ', 'bracket: ', JSON.stringify(bracket), 'startPosition: ', String(position)); - - const languageId = bracket.languageId; - const reversedBracketRegex = bracket.reversedRegex; - let count = -1; - - let totalCallCount = 0; - const searchPrevMatchingBracketInRange = (lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): Range | null | BracketSearchCanceled => { - while (true) { - if (continueSearchPredicate && (++totalCallCount) % 100 === 0 && !continueSearchPredicate()) { - return BracketSearchCanceled.INSTANCE; - } - const r = BracketsUtils.findPrevBracketInRange(reversedBracketRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (!r) { - break; - } - - const hitText = lineText.substring(r.startColumn - 1, r.endColumn - 1).toLowerCase(); - if (bracket.isOpen(hitText)) { - count++; - } else if (bracket.isClose(hitText)) { - count--; - } - - if (count === 0) { - return r; - } - - searchEndOffset = r.startColumn - 1; - } - - return null; - }; - - for (let lineNumber = position.lineNumber; lineNumber >= 1; lineNumber--) { - const lineTokens = this._getLineTokens(lineNumber); - const tokenCount = lineTokens.getCount(); - const lineText = this._buffer.getLineContent(lineNumber); - - let tokenIndex = tokenCount - 1; - let searchStartOffset = lineText.length; - let searchEndOffset = lineText.length; - if (lineNumber === position.lineNumber) { - tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); - searchStartOffset = position.column - 1; - searchEndOffset = position.column - 1; - } - - let prevSearchInToken = true; - for (; tokenIndex >= 0; tokenIndex--) { - const searchInToken = (lineTokens.getLanguageId(tokenIndex) === languageId && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); - - if (searchInToken) { - // this token should be searched - if (prevSearchInToken) { - // the previous token should be searched, simply extend searchStartOffset - searchStartOffset = lineTokens.getStartOffset(tokenIndex); - } else { - // the previous token should not be searched - searchStartOffset = lineTokens.getStartOffset(tokenIndex); - searchEndOffset = lineTokens.getEndOffset(tokenIndex); - } - } else { - // this token should not be searched - if (prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = searchPrevMatchingBracketInRange(lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return r; - } - } - } - - prevSearchInToken = searchInToken; - } - - if (prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = searchPrevMatchingBracketInRange(lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return r; - } - } - } - - return null; - } - - private _findMatchingBracketDown(bracket: RichEditBracket, position: Position, continueSearchPredicate: ContinueBracketSearchPredicate): Range | null | BracketSearchCanceled { - // console.log('_findMatchingBracketDown: ', 'bracket: ', JSON.stringify(bracket), 'startPosition: ', String(position)); - - const languageId = bracket.languageId; - const bracketRegex = bracket.forwardRegex; - let count = 1; - - let totalCallCount = 0; - const searchNextMatchingBracketInRange = (lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): Range | null | BracketSearchCanceled => { - while (true) { - if (continueSearchPredicate && (++totalCallCount) % 100 === 0 && !continueSearchPredicate()) { - return BracketSearchCanceled.INSTANCE; - } - const r = BracketsUtils.findNextBracketInRange(bracketRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (!r) { - break; - } - - const hitText = lineText.substring(r.startColumn - 1, r.endColumn - 1).toLowerCase(); - if (bracket.isOpen(hitText)) { - count++; - } else if (bracket.isClose(hitText)) { - count--; - } - - if (count === 0) { - return r; - } - - searchStartOffset = r.endColumn - 1; - } - - return null; - }; - - const lineCount = this.getLineCount(); - for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { - const lineTokens = this._getLineTokens(lineNumber); - const tokenCount = lineTokens.getCount(); - const lineText = this._buffer.getLineContent(lineNumber); - - let tokenIndex = 0; - let searchStartOffset = 0; - let searchEndOffset = 0; - if (lineNumber === position.lineNumber) { - tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); - searchStartOffset = position.column - 1; - searchEndOffset = position.column - 1; - } - - let prevSearchInToken = true; - for (; tokenIndex < tokenCount; tokenIndex++) { - const searchInToken = (lineTokens.getLanguageId(tokenIndex) === languageId && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); - - if (searchInToken) { - // this token should be searched - if (prevSearchInToken) { - // the previous token should be searched, simply extend searchEndOffset - searchEndOffset = lineTokens.getEndOffset(tokenIndex); - } else { - // the previous token should not be searched - searchStartOffset = lineTokens.getStartOffset(tokenIndex); - searchEndOffset = lineTokens.getEndOffset(tokenIndex); - } - } else { - // this token should not be searched - if (prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = searchNextMatchingBracketInRange(lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return r; - } - } - } - - prevSearchInToken = searchInToken; - } - - if (prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = searchNextMatchingBracketInRange(lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return r; - } - } - } - - return null; - } - - public findPrevBracket(_position: IPosition): model.IFoundBracket | null { - const position = this.validatePosition(_position); - - let languageId: string | null = null; - let modeBrackets: RichEditBrackets | null = null; - for (let lineNumber = position.lineNumber; lineNumber >= 1; lineNumber--) { - const lineTokens = this._getLineTokens(lineNumber); - const tokenCount = lineTokens.getCount(); - const lineText = this._buffer.getLineContent(lineNumber); - - let tokenIndex = tokenCount - 1; - let searchStartOffset = lineText.length; - let searchEndOffset = lineText.length; - if (lineNumber === position.lineNumber) { - tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); - searchStartOffset = position.column - 1; - searchEndOffset = position.column - 1; - const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); - if (languageId !== tokenLanguageId) { - languageId = tokenLanguageId; - modeBrackets = this.getLanguageConfiguration(languageId).brackets; - } - } - - let prevSearchInToken = true; - for (; tokenIndex >= 0; tokenIndex--) { - const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); - - if (languageId !== tokenLanguageId) { - // language id change! - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = BracketsUtils.findPrevBracketInRange(modeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return this._toFoundBracket(modeBrackets, r); - } - prevSearchInToken = false; - } - languageId = tokenLanguageId; - modeBrackets = this.getLanguageConfiguration(languageId).brackets; - } - - const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); - - if (searchInToken) { - // this token should be searched - if (prevSearchInToken) { - // the previous token should be searched, simply extend searchStartOffset - searchStartOffset = lineTokens.getStartOffset(tokenIndex); - } else { - // the previous token should not be searched - searchStartOffset = lineTokens.getStartOffset(tokenIndex); - searchEndOffset = lineTokens.getEndOffset(tokenIndex); - } - } else { - // this token should not be searched - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = BracketsUtils.findPrevBracketInRange(modeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return this._toFoundBracket(modeBrackets, r); - } - } - } - - prevSearchInToken = searchInToken; - } - - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = BracketsUtils.findPrevBracketInRange(modeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return this._toFoundBracket(modeBrackets, r); - } - } - } - - return null; - } - - public findNextBracket(_position: IPosition): model.IFoundBracket | null { - const position = this.validatePosition(_position); - const lineCount = this.getLineCount(); - - let languageId: string | null = null; - let modeBrackets: RichEditBrackets | null = null; - for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { - const lineTokens = this._getLineTokens(lineNumber); - const tokenCount = lineTokens.getCount(); - const lineText = this._buffer.getLineContent(lineNumber); - - let tokenIndex = 0; - let searchStartOffset = 0; - let searchEndOffset = 0; - if (lineNumber === position.lineNumber) { - tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); - searchStartOffset = position.column - 1; - searchEndOffset = position.column - 1; - const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); - if (languageId !== tokenLanguageId) { - languageId = tokenLanguageId; - modeBrackets = this.getLanguageConfiguration(languageId).brackets; - } - } - - let prevSearchInToken = true; - for (; tokenIndex < tokenCount; tokenIndex++) { - const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); - - if (languageId !== tokenLanguageId) { - // language id change! - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return this._toFoundBracket(modeBrackets, r); - } - prevSearchInToken = false; - } - languageId = tokenLanguageId; - modeBrackets = this.getLanguageConfiguration(languageId).brackets; - } - - const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); - if (searchInToken) { - // this token should be searched - if (prevSearchInToken) { - // the previous token should be searched, simply extend searchEndOffset - searchEndOffset = lineTokens.getEndOffset(tokenIndex); - } else { - // the previous token should not be searched - searchStartOffset = lineTokens.getStartOffset(tokenIndex); - searchEndOffset = lineTokens.getEndOffset(tokenIndex); - } - } else { - // this token should not be searched - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return this._toFoundBracket(modeBrackets, r); - } - } - } - - prevSearchInToken = searchInToken; - } - - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return this._toFoundBracket(modeBrackets, r); - } - } - } - - return null; - } - - public findEnclosingBrackets(_position: IPosition, maxDuration?: number): [Range, Range] | null { - let continueSearchPredicate: ContinueBracketSearchPredicate; - if (typeof maxDuration === 'undefined') { - continueSearchPredicate = null; - } else { - const startTime = Date.now(); - continueSearchPredicate = () => { - return (Date.now() - startTime <= maxDuration); - }; - } - const position = this.validatePosition(_position); - const lineCount = this.getLineCount(); - const savedCounts = new Map<string, number[]>(); - - let counts: number[] = []; - const resetCounts = (languageId: string, modeBrackets: RichEditBrackets | null) => { - if (!savedCounts.has(languageId)) { - let tmp = []; - for (let i = 0, len = modeBrackets ? modeBrackets.brackets.length : 0; i < len; i++) { - tmp[i] = 0; - } - savedCounts.set(languageId, tmp); - } - counts = savedCounts.get(languageId)!; - }; - - let totalCallCount = 0; - const searchInRange = (modeBrackets: RichEditBrackets, lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): [Range, Range] | null | BracketSearchCanceled => { - while (true) { - if (continueSearchPredicate && (++totalCallCount) % 100 === 0 && !continueSearchPredicate()) { - return BracketSearchCanceled.INSTANCE; - } - const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (!r) { - break; - } - - const hitText = lineText.substring(r.startColumn - 1, r.endColumn - 1).toLowerCase(); - const bracket = modeBrackets.textIsBracket[hitText]; - if (bracket) { - if (bracket.isOpen(hitText)) { - counts[bracket.index]++; - } else if (bracket.isClose(hitText)) { - counts[bracket.index]--; - } - - if (counts[bracket.index] === -1) { - return this._matchFoundBracket(r, bracket, false, continueSearchPredicate); - } - } - - searchStartOffset = r.endColumn - 1; - } - return null; - }; - - let languageId: string | null = null; - let modeBrackets: RichEditBrackets | null = null; - for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { - const lineTokens = this._getLineTokens(lineNumber); - const tokenCount = lineTokens.getCount(); - const lineText = this._buffer.getLineContent(lineNumber); - - let tokenIndex = 0; - let searchStartOffset = 0; - let searchEndOffset = 0; - if (lineNumber === position.lineNumber) { - tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); - searchStartOffset = position.column - 1; - searchEndOffset = position.column - 1; - const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); - if (languageId !== tokenLanguageId) { - languageId = tokenLanguageId; - modeBrackets = this.getLanguageConfiguration(languageId).brackets; - resetCounts(languageId, modeBrackets); - } - } - - let prevSearchInToken = true; - for (; tokenIndex < tokenCount; tokenIndex++) { - const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); - - if (languageId !== tokenLanguageId) { - // language id change! - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return stripBracketSearchCanceled(r); - } - prevSearchInToken = false; - } - languageId = tokenLanguageId; - modeBrackets = this.getLanguageConfiguration(languageId).brackets; - resetCounts(languageId, modeBrackets); - } - - const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); - if (searchInToken) { - // this token should be searched - if (prevSearchInToken) { - // the previous token should be searched, simply extend searchEndOffset - searchEndOffset = lineTokens.getEndOffset(tokenIndex); - } else { - // the previous token should not be searched - searchStartOffset = lineTokens.getStartOffset(tokenIndex); - searchEndOffset = lineTokens.getEndOffset(tokenIndex); - } - } else { - // this token should not be searched - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return stripBracketSearchCanceled(r); - } - } - } - - prevSearchInToken = searchInToken; - } - - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return stripBracketSearchCanceled(r); - } - } - } - - return null; - } - - private _toFoundBracket(modeBrackets: RichEditBrackets, r: Range): model.IFoundBracket | null { - if (!r) { - return null; - } - - let text = this.getValueInRange(r); - text = text.toLowerCase(); - - let data = modeBrackets.textIsBracket[text]; - if (!data) { - return null; - } - - return { - range: r, - open: data.open, - close: data.close, - isOpen: modeBrackets.textIsOpenBracket[text] - }; - } - /** * Returns: * - -1 => the line consists of whitespace diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 6b6085c4927..f2f40d007d4 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -483,6 +483,11 @@ export const enum CompletionItemInsertTextRule { InsertAsSnippet = 0b100, } +export interface CompletionItemRanges { + insert: IRange; + replace: IRange; +} + /** * A completion item represents a text snippet that is * proposed to complete text that is being typed. @@ -551,7 +556,7 @@ export interface CompletionItem { * *Note:* The range must be a {@link Range.isSingleLine single line} and it must * {@link Range.contains contain} the position at which completion has been {@link CompletionItemProvider.provideCompletionItems requested}. */ - range: IRange | { insert: IRange, replace: IRange }; + range: IRange | CompletionItemRanges; /** * An optional set of characters that when pressed while this completion is active will accept it first and * then type that character. *Note* that all commit characters should have `length=1` and that superfluous diff --git a/src/vs/editor/common/modes/tokenization/typescript.ts b/src/vs/editor/common/modes/tokenization/typescript.ts deleted file mode 100644 index 207e8f492ee..00000000000 --- a/src/vs/editor/common/modes/tokenization/typescript.ts +++ /dev/null @@ -1,304 +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 { StandardTokenType } from 'vs/editor/common/modes'; -import { CharCode } from 'vs/base/common/charCode'; - -class ParserContext { - public readonly text: string; - public readonly len: number; - public readonly tokens: number[]; - public pos: number; - - private currentTokenStartOffset: number; - private currentTokenType: StandardTokenType; - - constructor(text: string) { - this.text = text; - this.len = this.text.length; - this.tokens = []; - this.pos = 0; - this.currentTokenStartOffset = 0; - this.currentTokenType = StandardTokenType.Other; - } - - private _safeCharCodeAt(index: number): number { - if (index >= this.len) { - return CharCode.Null; - } - return this.text.charCodeAt(index); - } - - peek(distance: number = 0): number { - return this._safeCharCodeAt(this.pos + distance); - } - - next(): number { - const result = this._safeCharCodeAt(this.pos); - this.pos++; - return result; - } - - advance(distance: number): void { - this.pos += distance; - } - - eof(): boolean { - return this.pos >= this.len; - } - - beginToken(tokenType: StandardTokenType, deltaPos: number = 0): void { - this.currentTokenStartOffset = this.pos + deltaPos; - this.currentTokenType = tokenType; - } - - endToken(deltaPos: number = 0): void { - const length = this.pos + deltaPos - this.currentTokenStartOffset; - // check if it is touching previous token - if (this.tokens.length > 0) { - const previousStartOffset = this.tokens[this.tokens.length - 3]; - const previousLength = this.tokens[this.tokens.length - 2]; - const previousTokenType = this.tokens[this.tokens.length - 1]; - const previousEndOffset = previousStartOffset + previousLength; - if (this.currentTokenStartOffset === previousEndOffset && previousTokenType === this.currentTokenType) { - // extend previous token - this.tokens[this.tokens.length - 2] += length; - return; - } - } - this.tokens.push(this.currentTokenStartOffset, length, this.currentTokenType); - } -} - -export function parse(text: string): number[] { - const ctx = new ParserContext(text); - while (!ctx.eof()) { - parseRoot(ctx); - } - return ctx.tokens; -} - -function parseRoot(ctx: ParserContext): void { - let curlyCount = 0; - while (!ctx.eof()) { - const ch = ctx.peek(); - - switch (ch) { - case CharCode.SingleQuote: - parseSimpleString(ctx, CharCode.SingleQuote); - break; - case CharCode.DoubleQuote: - parseSimpleString(ctx, CharCode.DoubleQuote); - break; - case CharCode.BackTick: - parseInterpolatedString(ctx); - break; - case CharCode.Slash: - parseSlash(ctx); - break; - case CharCode.OpenCurlyBrace: - ctx.advance(1); - curlyCount++; - break; - case CharCode.CloseCurlyBrace: - ctx.advance(1); - curlyCount--; - if (curlyCount < 0) { - return; - } - break; - default: - ctx.advance(1); - } - } - -} - -function parseSimpleString(ctx: ParserContext, closingQuote: number): void { - ctx.beginToken(StandardTokenType.String); - - // skip the opening quote - ctx.advance(1); - - while (!ctx.eof()) { - const ch = ctx.next(); - if (ch === CharCode.Backslash) { - // skip \r\n or any other character following a backslash - const advanceCount = (ctx.peek() === CharCode.CarriageReturn && ctx.peek(1) === CharCode.LineFeed ? 2 : 1); - ctx.advance(advanceCount); - } else if (ch === closingQuote) { - // hit end quote, so stop - break; - } - } - - ctx.endToken(); -} - -function parseInterpolatedString(ctx: ParserContext): void { - ctx.beginToken(StandardTokenType.String); - - // skip the opening quote - ctx.advance(1); - - while (!ctx.eof()) { - const ch = ctx.next(); - if (ch === CharCode.Backslash) { - // skip \r\n or any other character following a backslash - const advanceCount = (ctx.peek() === CharCode.CarriageReturn && ctx.peek(1) === CharCode.LineFeed ? 2 : 1); - ctx.advance(advanceCount); - } else if (ch === CharCode.BackTick) { - // hit end quote, so stop - break; - } else if (ch === CharCode.DollarSign) { - if (ctx.peek() === CharCode.OpenCurlyBrace) { - ctx.advance(1); - ctx.endToken(); - parseRoot(ctx); - ctx.beginToken(StandardTokenType.String, -1); - } - } - } - - ctx.endToken(); -} - -function parseSlash(ctx: ParserContext): void { - - const nextCh = ctx.peek(1); - if (nextCh === CharCode.Asterisk) { - parseMultiLineComment(ctx); - return; - } - - if (nextCh === CharCode.Slash) { - parseSingleLineComment(ctx); - return; - } - - if (tryParseRegex(ctx)) { - return; - } - - ctx.advance(1); -} - -function tryParseRegex(ctx: ParserContext): boolean { - // See https://www.ecma-international.org/ecma-262/10.0/index.html#prod-RegularExpressionLiteral - - // TODO: avoid regex... - let contentBefore = ctx.text.substr(ctx.pos - 100, 100); - if (/[a-zA-Z0-9](\s*)$/.test(contentBefore)) { - // Cannot start after an identifier - return false; - } - - let pos = 0; - let len = ctx.len - ctx.pos; - let inClass = false; - - // skip / - pos++; - - while (pos < len) { - const ch = ctx.peek(pos++); - - if (ch === CharCode.CarriageReturn || ch === CharCode.LineFeed) { - return false; - } - - if (ch === CharCode.Backslash) { - const nextCh = ctx.peek(); - if (nextCh === CharCode.CarriageReturn || nextCh === CharCode.LineFeed) { - return false; - } - // skip next character - pos++; - continue; - } - - if (inClass) { - - if (ch === CharCode.CloseSquareBracket) { - inClass = false; - continue; - } - - } else { - - if (ch === CharCode.Slash) { - // cannot be directly followed by a / - if (ctx.peek(pos) === CharCode.Slash) { - return false; - } - - // consume flags - do { - let nextCh = ctx.peek(pos); - if (nextCh >= CharCode.a && nextCh <= CharCode.z) { - pos++; - continue; - } else { - break; - } - } while (true); - - // TODO: avoid regex... - if (/^(\s*)(\.|;|\/|,|\)|\]|\}|$)/.test(ctx.text.substr(ctx.pos + pos))) { - // Must be followed by an operator of kinds - ctx.beginToken(StandardTokenType.RegEx); - ctx.advance(pos); - ctx.endToken(); - return true; - } - - return false; - } - - if (ch === CharCode.OpenSquareBracket) { - inClass = true; - continue; - } - - } - } - - return false; -} - -function parseMultiLineComment(ctx: ParserContext): void { - ctx.beginToken(StandardTokenType.Comment); - - // skip the /* - ctx.advance(2); - - while (!ctx.eof()) { - const ch = ctx.next(); - if (ch === CharCode.Asterisk) { - if (ctx.peek() === CharCode.Slash) { - ctx.advance(1); - break; - } - } - } - - ctx.endToken(); -} - -function parseSingleLineComment(ctx: ParserContext): void { - ctx.beginToken(StandardTokenType.Comment); - - // skip the // - ctx.advance(2); - - while (!ctx.eof()) { - const ch = ctx.next(); - if (ch === CharCode.CarriageReturn || ch === CharCode.LineFeed) { - break; - } - } - - ctx.endToken(); -} diff --git a/src/vs/editor/common/view/editorColorRegistry.ts b/src/vs/editor/common/view/editorColorRegistry.ts index d0950a54331..e1619ace439 100644 --- a/src/vs/editor/common/view/editorColorRegistry.ts +++ b/src/vs/editor/common/view/editorColorRegistry.ts @@ -45,6 +45,7 @@ export const editorUnnecessaryCodeOpacity = registerColor('editorUnnecessaryCode export const ghostTextBorder = registerColor('editorGhostText.border', { dark: null, light: null, hc: Color.fromHex('#fff').transparent(0.8) }, nls.localize('editorGhostTextBorder', 'Border color of ghost text in the editor.')); export const ghostTextForeground = registerColor('editorGhostText.foreground', { dark: Color.fromHex('#ffffff56'), light: Color.fromHex('#0007'), hc: null }, nls.localize('editorGhostTextForeground', 'Foreground color of the ghost text in the editor.')); +export const ghostTextBackground = registerColor('editorGhostText.background', { dark: null, light: null, hc: null }, nls.localize('editorGhostTextBackground', 'Background color of the ghost text in the editor.')); const rulerRangeDefault = new Color(new RGBA(0, 122, 204, 0.6)); export const overviewRulerRangeHighlight = registerColor('editorOverviewRuler.rangeHighlightForeground', { dark: rulerRangeDefault, light: rulerRangeDefault, hc: rulerRangeDefault }, nls.localize('overviewRulerRangeHighlight', 'Overview ruler marker color for range highlights. The color must not be opaque so as not to hide underlying decorations.'), true); diff --git a/src/vs/editor/common/viewModel/prefixSumComputer.ts b/src/vs/editor/common/viewModel/prefixSumComputer.ts index c64f57195b9..52d6f316dcf 100644 --- a/src/vs/editor/common/viewModel/prefixSumComputer.ts +++ b/src/vs/editor/common/viewModel/prefixSumComputer.ts @@ -3,20 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { arrayInsert } from 'vs/base/common/arrays'; import { toUint32 } from 'vs/base/common/uint'; -export class PrefixSumIndexOfResult { - _prefixSumIndexOfResultBrand: void = undefined; - - index: number; - remainder: number; - - constructor(index: number, remainder: number) { - this.index = index; - this.remainder = remainder; - } -} - export class PrefixSumComputer { /** @@ -71,7 +60,7 @@ export class PrefixSumComputer { return true; } - public changeValue(index: number, value: number): boolean { + public setValue(index: number, value: number): boolean { index = toUint32(index); value = toUint32(value); @@ -187,3 +176,119 @@ export class PrefixSumComputer { return new PrefixSumIndexOfResult(mid, sum - midStart); } } + +/** + * {@link getIndexOf} has an amortized runtime complexity of O(1). + * + * ({@link PrefixSumComputer.getIndexOf} is just O(log n)) +*/ +export class ConstantTimePrefixSumComputer { + private _values: number[]; + private _isValid: boolean; + private _validEndIndex: number; + + /** + * _prefixSum[i] = SUM(values[j]), 0 <= j <= i + */ + private _prefixSum: number[]; + + /** + * _indexBySum[sum] = idx => _prefixSum[idx - 1] <= sum < _prefixSum[idx] + */ + private _indexBySum: number[]; + + constructor(values: number[]) { + this._values = values; + this._isValid = false; + this._validEndIndex = -1; + this._prefixSum = []; + this._indexBySum = []; + } + + /** + * @returns SUM(0 <= j < values.length, values[j]) + */ + public getTotalSum(): number { + this._ensureValid(); + return this._indexBySum.length; + } + + /** + * @returns `SUM(0 <= j <= index, values[j])`. Includes `values[index]`! + */ + public getPrefixSum(index: number): number { + this._ensureValid(); + return this._prefixSum[index]; + } + + /** + * @returns `result`, such that `getPrefixSum(result.index - 1) + result.remainder = sum` + */ + public getIndexOf(sum: number): PrefixSumIndexOfResult { + this._ensureValid(); + const idx = this._indexBySum[sum]; + const viewLinesAbove = idx > 0 ? this._prefixSum[idx - 1] : 0; + return new PrefixSumIndexOfResult(idx, sum - viewLinesAbove); + } + + public removeValues(start: number, deleteCount: number): void { + this._values.splice(start, deleteCount); + this._invalidate(start); + } + + public insertValues(insertIndex: number, insertArr: number[]): void { + this._values = arrayInsert(this._values, insertIndex, insertArr); + this._invalidate(insertIndex); + } + + private _invalidate(index: number): void { + this._isValid = false; + this._validEndIndex = Math.min(this._validEndIndex, index - 1); + } + + private _ensureValid(): void { + if (this._isValid) { + return; + } + + for (let i = this._validEndIndex + 1, len = this._values.length; i < len; i++) { + const value = this._values[i]; + const sumAbove = i > 0 ? this._prefixSum[i - 1] : 0; + + this._prefixSum[i] = sumAbove + value; + for (let j = 0; j < value; j++) { + this._indexBySum[sumAbove + j] = i; + } + } + + // trim things + this._prefixSum.length = this._values.length; + this._indexBySum.length = this._prefixSum[this._prefixSum.length - 1]; + + // mark as valid + this._isValid = true; + this._validEndIndex = this._values.length - 1; + } + + public setValue(index: number, value: number): void { + if (this._values[index] === value) { + // no change + return; + } + this._values[index] = value; + this._invalidate(index); + } +} + + +export class PrefixSumIndexOfResult { + _prefixSumIndexOfResultBrand: void = undefined; + + constructor( + public readonly index: number, + public readonly remainder: number + ) { + this.index = index; + this.remainder = remainder; + } +} diff --git a/src/vs/editor/common/viewModel/splitLinesCollection.ts b/src/vs/editor/common/viewModel/splitLinesCollection.ts index 6fb158d78a3..7b998c5a808 100644 --- a/src/vs/editor/common/viewModel/splitLinesCollection.ts +++ b/src/vs/editor/common/viewModel/splitLinesCollection.ts @@ -11,11 +11,11 @@ import { IRange, Range } from 'vs/editor/common/core/range'; import { BracketGuideOptions, EndOfLinePreference, IActiveIndentGuideInfo, IModelDecoration, IModelDeltaDecoration, IndentGuide, IndentGuideHorizontalLine, ITextModel, PositionAffinity } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; -import { PrefixSumIndexOfResult } from 'vs/editor/common/viewModel/prefixSumComputer'; import { ICoordinatesConverter, InjectedText, ILineBreaksComputer, LineBreakData, SingleLineInlineDecoration, ViewLineData } from 'vs/editor/common/viewModel/viewModel'; import { IDisposable } from 'vs/base/common/lifecycle'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; import { LineInjectedText } from 'vs/editor/common/model/textModelEvents'; +import { ConstantTimePrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; export interface ILineBreaksComputerFactory { createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent): ILineBreaksComputer; @@ -30,9 +30,9 @@ export interface ISimpleModel { getValueInRange(range: IRange, eol?: EndOfLinePreference): string; } -export interface ISplitLine { +export interface IModelLineProjection { isVisible(): boolean; - setVisible(isVisible: boolean): ISplitLine; + setVisible(isVisible: boolean): IModelLineProjection; getLineBreakData(): LineBreakData | null; getViewLineCount(): number; @@ -144,89 +144,6 @@ const enum IndentGuideRepeatOption { BlockAll = 2 } -class LineNumberMapper { - - private _counts: number[]; - private _isValid: boolean; - private _validEndIndex: number; - - private _modelToView: number[]; - private _viewToModel: number[]; - - constructor(viewLineCounts: number[]) { - this._counts = viewLineCounts; - this._isValid = false; - this._validEndIndex = -1; - this._modelToView = []; - this._viewToModel = []; - } - - private _invalidate(index: number): void { - this._isValid = false; - this._validEndIndex = Math.min(this._validEndIndex, index - 1); - } - - private _ensureValid(): void { - if (this._isValid) { - return; - } - - for (let i = this._validEndIndex + 1, len = this._counts.length; i < len; i++) { - const viewLineCount = this._counts[i]; - const viewLinesAbove = (i > 0 ? this._modelToView[i - 1] : 0); - - this._modelToView[i] = viewLinesAbove + viewLineCount; - for (let j = 0; j < viewLineCount; j++) { - this._viewToModel[viewLinesAbove + j] = i; - } - } - - // trim things - this._modelToView.length = this._counts.length; - this._viewToModel.length = this._modelToView[this._modelToView.length - 1]; - - // mark as valid - this._isValid = true; - this._validEndIndex = this._counts.length - 1; - } - - public changeValue(index: number, value: number): void { - if (this._counts[index] === value) { - // no change - return; - } - this._counts[index] = value; - this._invalidate(index); - } - - public removeValues(start: number, deleteCount: number): void { - this._counts.splice(start, deleteCount); - this._invalidate(start); - } - - public insertValues(insertIndex: number, insertArr: number[]): void { - this._counts = arrays.arrayInsert(this._counts, insertIndex, insertArr); - this._invalidate(insertIndex); - } - - public getTotalValue(): number { - this._ensureValid(); - return this._viewToModel.length; - } - - public getAccumulatedValue(index: number): number { - this._ensureValid(); - return this._modelToView[index]; - } - - public getIndexOf(accumulatedValue: number): PrefixSumIndexOfResult { - this._ensureValid(); - const modelLineIndex = this._viewToModel[accumulatedValue]; - const viewLinesAbove = (modelLineIndex > 0 ? this._modelToView[modelLineIndex - 1] : 0); - return new PrefixSumIndexOfResult(modelLineIndex, accumulatedValue - viewLinesAbove); - } -} - export class SplitLinesCollection implements IViewModelLinesCollection { private readonly _editorId: number; @@ -241,9 +158,13 @@ export class SplitLinesCollection implements IViewModelLinesCollection { private wrappingColumn: number; private wrappingIndent: WrappingIndent; private wrappingStrategy: 'simple' | 'advanced'; - private lines!: ISplitLine[]; - private prefixSumComputer!: LineNumberMapper; + private modelLineProjections!: IModelLineProjection[]; + + /** + * Reflects the sum of the line counts of all . + */ + private projectedModelLineLineCounts!: ConstantTimePrefixSumComputer; private hiddenAreasIds!: string[]; @@ -281,7 +202,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } private _constructLines(resetHiddenAreas: boolean, previousLineBreaks: ((LineBreakData | null)[]) | null): void { - this.lines = []; + this.modelLineProjections = []; if (resetHiddenAreas) { this.hiddenAreasIds = []; @@ -317,14 +238,14 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } let isInHiddenArea = (lineNumber >= hiddenAreaStart && lineNumber <= hiddenAreaEnd); - let line = createSplitLine(linesBreaks[i], !isInHiddenArea); + let line = createModelLineProjection(linesBreaks[i], !isInHiddenArea); values[i] = line.getViewLineCount(); - this.lines[i] = line; + this.modelLineProjections[i] = line; } this._validModelVersionId = this.model.getVersionId(); - this.prefixSumComputer = new LineNumberMapper(values); + this.projectedModelLineLineCounts = new ConstantTimePrefixSumComputer(values); } public getHiddenAreas(): Range[] { @@ -392,37 +313,37 @@ export class SplitLinesCollection implements IViewModelLinesCollection { let hiddenAreas = newRanges; let hiddenAreaStart = 1, hiddenAreaEnd = 0; let hiddenAreaIdx = -1; - let nextLineNumberToUpdateHiddenArea = (hiddenAreaIdx + 1 < hiddenAreas.length) ? hiddenAreaEnd + 1 : this.lines.length + 2; + let nextLineNumberToUpdateHiddenArea = (hiddenAreaIdx + 1 < hiddenAreas.length) ? hiddenAreaEnd + 1 : this.modelLineProjections.length + 2; let hasVisibleLine = false; - for (let i = 0; i < this.lines.length; i++) { + for (let i = 0; i < this.modelLineProjections.length; i++) { let lineNumber = i + 1; if (lineNumber === nextLineNumberToUpdateHiddenArea) { hiddenAreaIdx++; hiddenAreaStart = hiddenAreas[hiddenAreaIdx].startLineNumber; hiddenAreaEnd = hiddenAreas[hiddenAreaIdx].endLineNumber; - nextLineNumberToUpdateHiddenArea = (hiddenAreaIdx + 1 < hiddenAreas.length) ? hiddenAreaEnd + 1 : this.lines.length + 2; + nextLineNumberToUpdateHiddenArea = (hiddenAreaIdx + 1 < hiddenAreas.length) ? hiddenAreaEnd + 1 : this.modelLineProjections.length + 2; } let lineChanged = false; if (lineNumber >= hiddenAreaStart && lineNumber <= hiddenAreaEnd) { // Line should be hidden - if (this.lines[i].isVisible()) { - this.lines[i] = this.lines[i].setVisible(false); + if (this.modelLineProjections[i].isVisible()) { + this.modelLineProjections[i] = this.modelLineProjections[i].setVisible(false); lineChanged = true; } } else { hasVisibleLine = true; // Line should be visible - if (!this.lines[i].isVisible()) { - this.lines[i] = this.lines[i].setVisible(true); + if (!this.modelLineProjections[i].isVisible()) { + this.modelLineProjections[i] = this.modelLineProjections[i].setVisible(true); lineChanged = true; } } if (lineChanged) { - let newOutputLineCount = this.lines[i].getViewLineCount(); - this.prefixSumComputer.changeValue(i, newOutputLineCount); + let newOutputLineCount = this.modelLineProjections[i].getViewLineCount(); + this.projectedModelLineLineCounts.setValue(i, newOutputLineCount); } } @@ -435,19 +356,19 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public modelPositionIsVisible(modelLineNumber: number, _modelColumn: number): boolean { - if (modelLineNumber < 1 || modelLineNumber > this.lines.length) { + if (modelLineNumber < 1 || modelLineNumber > this.modelLineProjections.length) { // invalid arguments return false; } - return this.lines[modelLineNumber - 1].isVisible(); + return this.modelLineProjections[modelLineNumber - 1].isVisible(); } public getModelLineViewLineCount(modelLineNumber: number): number { - if (modelLineNumber < 1 || modelLineNumber > this.lines.length) { + if (modelLineNumber < 1 || modelLineNumber > this.modelLineProjections.length) { // invalid arguments return 1; } - return this.lines[modelLineNumber - 1].getViewLineCount(); + return this.modelLineProjections[modelLineNumber - 1].getViewLineCount(); } public setTabSize(newTabSize: number): boolean { @@ -480,8 +401,8 @@ export class SplitLinesCollection implements IViewModelLinesCollection { let previousLineBreaks: ((LineBreakData | null)[]) | null = null; if (onlyWrappingColumnChanged) { previousLineBreaks = []; - for (let i = 0, len = this.lines.length; i < len; i++) { - previousLineBreaks[i] = this.lines[i].getLineBreakData(); + for (let i = 0, len = this.modelLineProjections.length; i < len; i++) { + previousLineBreaks[i] = this.modelLineProjections[i].getLineBreakData(); } } @@ -510,11 +431,11 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return null; } - let outputFromLineNumber = (fromLineNumber === 1 ? 1 : this.prefixSumComputer.getAccumulatedValue(fromLineNumber - 2) + 1); - let outputToLineNumber = this.prefixSumComputer.getAccumulatedValue(toLineNumber - 1); + let outputFromLineNumber = (fromLineNumber === 1 ? 1 : this.projectedModelLineLineCounts.getPrefixSum(fromLineNumber - 2) + 1); + let outputToLineNumber = this.projectedModelLineLineCounts.getPrefixSum(toLineNumber - 1); - this.lines.splice(fromLineNumber - 1, toLineNumber - fromLineNumber + 1); - this.prefixSumComputer.removeValues(fromLineNumber - 1, toLineNumber - fromLineNumber + 1); + this.modelLineProjections.splice(fromLineNumber - 1, toLineNumber - fromLineNumber + 1); + this.projectedModelLineLineCounts.removeValues(fromLineNumber - 1, toLineNumber - fromLineNumber + 1); return new viewEvents.ViewLinesDeletedEvent(outputFromLineNumber, outputToLineNumber); } @@ -527,16 +448,16 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } // 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()); + const isInHiddenArea = (fromLineNumber > 2 && !this.modelLineProjections[fromLineNumber - 2].isVisible()); - let outputFromLineNumber = (fromLineNumber === 1 ? 1 : this.prefixSumComputer.getAccumulatedValue(fromLineNumber - 2) + 1); + let outputFromLineNumber = (fromLineNumber === 1 ? 1 : this.projectedModelLineLineCounts.getPrefixSum(fromLineNumber - 2) + 1); let totalOutputLineCount = 0; - let insertLines: ISplitLine[] = []; + let insertLines: IModelLineProjection[] = []; let insertPrefixSumValues: number[] = []; for (let i = 0, len = lineBreaks.length; i < len; i++) { - let line = createSplitLine(lineBreaks[i], !isInHiddenArea); + let line = createModelLineProjection(lineBreaks[i], !isInHiddenArea); insertLines.push(line); let outputLineCount = line.getViewLineCount(); @@ -545,9 +466,9 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } // TODO@Alex: use arrays.arrayInsert - this.lines = this.lines.slice(0, fromLineNumber - 1).concat(insertLines).concat(this.lines.slice(fromLineNumber - 1)); + this.modelLineProjections = this.modelLineProjections.slice(0, fromLineNumber - 1).concat(insertLines).concat(this.modelLineProjections.slice(fromLineNumber - 1)); - this.prefixSumComputer.insertValues(fromLineNumber - 1, insertPrefixSumValues); + this.projectedModelLineLineCounts.insertValues(fromLineNumber - 1, insertPrefixSumValues); return new viewEvents.ViewLinesInsertedEvent(outputFromLineNumber, outputFromLineNumber + totalOutputLineCount - 1); } @@ -561,11 +482,11 @@ export class SplitLinesCollection implements IViewModelLinesCollection { let lineIndex = lineNumber - 1; - let oldOutputLineCount = this.lines[lineIndex].getViewLineCount(); - let isVisible = this.lines[lineIndex].isVisible(); - let line = createSplitLine(lineBreakData, isVisible); - this.lines[lineIndex] = line; - let newOutputLineCount = this.lines[lineIndex].getViewLineCount(); + let oldOutputLineCount = this.modelLineProjections[lineIndex].getViewLineCount(); + let isVisible = this.modelLineProjections[lineIndex].isVisible(); + let line = createModelLineProjection(lineBreakData, isVisible); + this.modelLineProjections[lineIndex] = line; + let newOutputLineCount = this.modelLineProjections[lineIndex].getViewLineCount(); let lineMappingChanged = false; let changeFrom = 0; @@ -576,23 +497,23 @@ export class SplitLinesCollection implements IViewModelLinesCollection { let deleteTo = -1; if (oldOutputLineCount > newOutputLineCount) { - changeFrom = (lineNumber === 1 ? 1 : this.prefixSumComputer.getAccumulatedValue(lineNumber - 2) + 1); + changeFrom = (lineNumber === 1 ? 1 : this.projectedModelLineLineCounts.getPrefixSum(lineNumber - 2) + 1); changeTo = changeFrom + newOutputLineCount - 1; deleteFrom = changeTo + 1; deleteTo = deleteFrom + (oldOutputLineCount - newOutputLineCount) - 1; lineMappingChanged = true; } else if (oldOutputLineCount < newOutputLineCount) { - changeFrom = (lineNumber === 1 ? 1 : this.prefixSumComputer.getAccumulatedValue(lineNumber - 2) + 1); + changeFrom = (lineNumber === 1 ? 1 : this.projectedModelLineLineCounts.getPrefixSum(lineNumber - 2) + 1); changeTo = changeFrom + oldOutputLineCount - 1; insertFrom = changeTo + 1; insertTo = insertFrom + (newOutputLineCount - oldOutputLineCount) - 1; lineMappingChanged = true; } else { - changeFrom = (lineNumber === 1 ? 1 : this.prefixSumComputer.getAccumulatedValue(lineNumber - 2) + 1); + changeFrom = (lineNumber === 1 ? 1 : this.projectedModelLineLineCounts.getPrefixSum(lineNumber - 2) + 1); changeTo = changeFrom + newOutputLineCount - 1; } - this.prefixSumComputer.changeValue(lineIndex, newOutputLineCount); + this.projectedModelLineLineCounts.setValue(lineIndex, newOutputLineCount); const viewLinesChangedEvent = (changeFrom <= changeTo ? new viewEvents.ViewLinesChangedEvent(changeFrom, changeTo) : null); const viewLinesInsertedEvent = (insertFrom <= insertTo ? new viewEvents.ViewLinesInsertedEvent(insertFrom, insertTo) : null); @@ -603,14 +524,14 @@ export class SplitLinesCollection implements IViewModelLinesCollection { public acceptVersionId(versionId: number): void { this._validModelVersionId = versionId; - if (this.lines.length === 1 && !this.lines[0].isVisible()) { + if (this.modelLineProjections.length === 1 && !this.modelLineProjections[0].isVisible()) { // At least one line must be visible => reset hidden areas this.setHiddenAreas([]); } } public getViewLineCount(): number { - return this.prefixSumComputer.getTotalValue(); + return this.projectedModelLineLineCounts.getTotalSum(); } private _toValidViewLineNumber(viewLineNumber: number): number { @@ -645,16 +566,16 @@ export class SplitLinesCollection implements IViewModelLinesCollection { // #region ViewLineInfo - public getViewLineInfo(viewLineNumber: number): ViewLineInfo { + private getViewLineInfo(viewLineNumber: number): ViewLineInfo { viewLineNumber = this._toValidViewLineNumber(viewLineNumber); - let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); + let r = this.projectedModelLineLineCounts.getIndexOf(viewLineNumber - 1); let lineIndex = r.index; let remainder = r.remainder; return new ViewLineInfo(lineIndex + 1, remainder); } private getMinColumnOfViewLine(viewLineInfo: ViewLineInfo): number { - return this.lines[viewLineInfo.modelLineNumber - 1].getViewLineMinColumn( + return this.modelLineProjections[viewLineInfo.modelLineNumber - 1].getViewLineMinColumn( this.model, viewLineInfo.modelLineNumber, viewLineInfo.modelLineWrappedLineIdx @@ -662,7 +583,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } private getModelStartPositionOfViewLine(viewLineInfo: ViewLineInfo): Position { - const line = this.lines[viewLineInfo.modelLineNumber - 1]; + const line = this.modelLineProjections[viewLineInfo.modelLineNumber - 1]; const minViewColumn = line.getViewLineMinColumn( this.model, viewLineInfo.modelLineNumber, @@ -676,7 +597,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } private getModelEndPositionOfViewLine(viewLineInfo: ViewLineInfo): Position { - const line = this.lines[viewLineInfo.modelLineNumber - 1]; + const line = this.modelLineProjections[viewLineInfo.modelLineNumber - 1]; const maxViewColumn = line.getViewLineMaxColumn( this.model, viewLineInfo.modelLineNumber, @@ -698,7 +619,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { let viewLines = new Array<ViewLineInfo>(); for (let curModelLine = startViewLine.modelLineNumber; curModelLine <= endViewLine.modelLineNumber; curModelLine++) { - const line = this.lines[curModelLine - 1]; + const line = this.modelLineProjections[curModelLine - 1]; if (line.isVisible()) { let startOffset = @@ -794,7 +715,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { let reqStart: Position | null = null; for (let modelLineIndex = modelStartLineIndex; modelLineIndex <= modelEndLineIndex; modelLineIndex++) { - const line = this.lines[modelLineIndex]; + const line = this.modelLineProjections[modelLineIndex]; if (line.isVisible()) { let viewLineStartIndex = line.getViewLineNumberOfModelPosition(0, modelLineIndex === modelStartLineIndex ? modelStart.column : 1); let viewLineEndIndex = line.getViewLineNumberOfModelPosition(0, this.model.getLineMaxColumn(modelLineIndex + 1)); @@ -850,48 +771,28 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public getViewLineContent(viewLineNumber: number): string { - viewLineNumber = this._toValidViewLineNumber(viewLineNumber); - let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); - let lineIndex = r.index; - let remainder = r.remainder; - - return this.lines[lineIndex].getViewLineContent(this.model, lineIndex + 1, remainder); + const info = this.getViewLineInfo(viewLineNumber); + return this.modelLineProjections[info.modelLineNumber - 1].getViewLineContent(this.model, info.modelLineNumber, info.modelLineWrappedLineIdx); } public getViewLineLength(viewLineNumber: number): number { - viewLineNumber = this._toValidViewLineNumber(viewLineNumber); - let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); - let lineIndex = r.index; - let remainder = r.remainder; - - return this.lines[lineIndex].getViewLineLength(this.model, lineIndex + 1, remainder); + const info = this.getViewLineInfo(viewLineNumber); + return this.modelLineProjections[info.modelLineNumber - 1].getViewLineLength(this.model, info.modelLineNumber, info.modelLineWrappedLineIdx); } public getViewLineMinColumn(viewLineNumber: number): number { - viewLineNumber = this._toValidViewLineNumber(viewLineNumber); - let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); - let lineIndex = r.index; - let remainder = r.remainder; - - return this.lines[lineIndex].getViewLineMinColumn(this.model, lineIndex + 1, remainder); + const info = this.getViewLineInfo(viewLineNumber); + return this.modelLineProjections[info.modelLineNumber - 1].getViewLineMinColumn(this.model, info.modelLineNumber, info.modelLineWrappedLineIdx); } public getViewLineMaxColumn(viewLineNumber: number): number { - viewLineNumber = this._toValidViewLineNumber(viewLineNumber); - let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); - let lineIndex = r.index; - let remainder = r.remainder; - - return this.lines[lineIndex].getViewLineMaxColumn(this.model, lineIndex + 1, remainder); + const info = this.getViewLineInfo(viewLineNumber); + return this.modelLineProjections[info.modelLineNumber - 1].getViewLineMaxColumn(this.model, info.modelLineNumber, info.modelLineWrappedLineIdx); } public getViewLineData(viewLineNumber: number): ViewLineData { - viewLineNumber = this._toValidViewLineNumber(viewLineNumber); - let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); - let lineIndex = r.index; - let remainder = r.remainder; - - return this.lines[lineIndex].getViewLineData(this.model, lineIndex + 1, remainder); + const info = this.getViewLineInfo(viewLineNumber); + return this.modelLineProjections[info.modelLineNumber - 1].getViewLineData(this.model, info.modelLineNumber, info.modelLineWrappedLineIdx); } public getViewLinesData(viewStartLineNumber: number, viewEndLineNumber: number, needed: boolean[]): ViewLineData[] { @@ -899,14 +800,14 @@ export class SplitLinesCollection implements IViewModelLinesCollection { viewStartLineNumber = this._toValidViewLineNumber(viewStartLineNumber); viewEndLineNumber = this._toValidViewLineNumber(viewEndLineNumber); - let start = this.prefixSumComputer.getIndexOf(viewStartLineNumber - 1); + let start = this.projectedModelLineLineCounts.getIndexOf(viewStartLineNumber - 1); let viewLineNumber = viewStartLineNumber; let startModelLineIndex = start.index; let startRemainder = start.remainder; let result: ViewLineData[] = []; for (let modelLineIndex = startModelLineIndex, len = this.model.getLineCount(); modelLineIndex < len; modelLineIndex++) { - let line = this.lines[modelLineIndex]; + let line = this.modelLineProjections[modelLineIndex]; if (!line.isVisible()) { continue; } @@ -935,11 +836,11 @@ export class SplitLinesCollection implements IViewModelLinesCollection { public validateViewPosition(viewLineNumber: number, viewColumn: number, expectedModelPosition: Position): Position { viewLineNumber = this._toValidViewLineNumber(viewLineNumber); - let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); + let r = this.projectedModelLineLineCounts.getIndexOf(viewLineNumber - 1); let lineIndex = r.index; let remainder = r.remainder; - let line = this.lines[lineIndex]; + let line = this.modelLineProjections[lineIndex]; let minColumn = line.getViewLineMinColumn(this.model, lineIndex + 1, remainder); let maxColumn = line.getViewLineMaxColumn(this.model, lineIndex + 1, remainder); @@ -967,15 +868,11 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public convertViewPositionToModelPosition(viewLineNumber: number, viewColumn: number): Position { - viewLineNumber = this._toValidViewLineNumber(viewLineNumber); + const info = this.getViewLineInfo(viewLineNumber); - let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); - let lineIndex = r.index; - let remainder = r.remainder; - - let inputColumn = this.lines[lineIndex].getModelColumnOfViewPosition(remainder, viewColumn); + let inputColumn = this.modelLineProjections[info.modelLineNumber - 1].getModelColumnOfViewPosition(info.modelLineWrappedLineIdx, viewColumn); // console.log('out -> in ' + viewLineNumber + ',' + viewColumn + ' ===> ' + (lineIndex+1) + ',' + inputColumn); - return this.model.validatePosition(new Position(lineIndex + 1, inputColumn)); + return this.model.validatePosition(new Position(info.modelLineNumber, inputColumn)); } public convertViewRangeToModelRange(viewRange: Range): Range { @@ -991,22 +888,22 @@ export class SplitLinesCollection implements IViewModelLinesCollection { const inputColumn = validPosition.column; let lineIndex = inputLineNumber - 1, lineIndexChanged = false; - while (lineIndex > 0 && !this.lines[lineIndex].isVisible()) { + while (lineIndex > 0 && !this.modelLineProjections[lineIndex].isVisible()) { lineIndex--; lineIndexChanged = true; } - if (lineIndex === 0 && !this.lines[lineIndex].isVisible()) { + if (lineIndex === 0 && !this.modelLineProjections[lineIndex].isVisible()) { // Could not reach a real line // console.log('in -> out ' + inputLineNumber + ',' + inputColumn + ' ===> ' + 1 + ',' + 1); return new Position(1, 1); } - const deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.prefixSumComputer.getAccumulatedValue(lineIndex - 1)); + const deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.projectedModelLineLineCounts.getPrefixSum(lineIndex - 1)); let r: Position; if (lineIndexChanged) { - r = this.lines[lineIndex].getViewPositionOfModelPosition(deltaLineNumber, this.model.getLineMaxColumn(lineIndex + 1), affinity); + r = this.modelLineProjections[lineIndex].getViewPositionOfModelPosition(deltaLineNumber, this.model.getLineMaxColumn(lineIndex + 1), affinity); } else { - r = this.lines[inputLineNumber - 1].getViewPositionOfModelPosition(deltaLineNumber, inputColumn, affinity); + r = this.modelLineProjections[inputLineNumber - 1].getViewPositionOfModelPosition(deltaLineNumber, inputColumn, affinity); } // console.log('in -> out ' + inputLineNumber + ',' + inputColumn + ' ===> ' + r.lineNumber + ',' + r); @@ -1027,24 +924,24 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } } - public getViewLineNumberOfModelPosition(inputLineNumber: number, inputColumn: number): number { - let lineIndex = inputLineNumber - 1; - if (this.lines[lineIndex].isVisible()) { + public getViewLineNumberOfModelPosition(modelLineNumber: number, modelColumn: number): number { + let lineIndex = modelLineNumber - 1; + if (this.modelLineProjections[lineIndex].isVisible()) { // this model line is visible - const deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.prefixSumComputer.getAccumulatedValue(lineIndex - 1)); - return this.lines[lineIndex].getViewLineNumberOfModelPosition(deltaLineNumber, inputColumn); + const deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.projectedModelLineLineCounts.getPrefixSum(lineIndex - 1)); + return this.modelLineProjections[lineIndex].getViewLineNumberOfModelPosition(deltaLineNumber, modelColumn); } // this model line is not visible - while (lineIndex > 0 && !this.lines[lineIndex].isVisible()) { + while (lineIndex > 0 && !this.modelLineProjections[lineIndex].isVisible()) { lineIndex--; } - if (lineIndex === 0 && !this.lines[lineIndex].isVisible()) { + if (lineIndex === 0 && !this.modelLineProjections[lineIndex].isVisible()) { // Could not reach a real line return 1; } - const deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.prefixSumComputer.getAccumulatedValue(lineIndex - 1)); - return this.lines[lineIndex].getViewLineNumberOfModelPosition(deltaLineNumber, this.model.getLineMaxColumn(lineIndex + 1)); + const deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.projectedModelLineLineCounts.getPrefixSum(lineIndex - 1)); + return this.modelLineProjections[lineIndex].getViewLineNumberOfModelPosition(deltaLineNumber, this.model.getLineMaxColumn(lineIndex + 1)); } public getDecorationsInRange(range: Range, ownerId: number, filterOutValidation: boolean): IModelDecoration[] { @@ -1063,7 +960,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { let reqStart: Position | null = null; for (let modelLineIndex = modelStartLineIndex; modelLineIndex <= modelEndLineIndex; modelLineIndex++) { - const line = this.lines[modelLineIndex]; + const line = this.modelLineProjections[modelLineIndex]; if (line.isVisible()) { // merge into previous request if (reqStart === null) { @@ -1115,31 +1012,19 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public getInjectedTextAt(position: Position): InjectedText | null { - const viewLineNumber = this._toValidViewLineNumber(position.lineNumber); - const r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); - const lineIndex = r.index; - const remainder = r.remainder; - - return this.lines[lineIndex].getInjectedTextAt(remainder, position.column); + const info = this.getViewLineInfo(position.lineNumber); + return this.modelLineProjections[info.modelLineNumber - 1].getInjectedTextAt(info.modelLineWrappedLineIdx, position.column); } normalizePosition(position: Position, affinity: PositionAffinity): Position { - const viewLineNumber = this._toValidViewLineNumber(position.lineNumber); - const r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); - const lineIndex = r.index; - const remainder = r.remainder; - - return this.lines[lineIndex].normalizePosition(this.model, lineIndex + 1, remainder, position, affinity); + const info = this.getViewLineInfo(position.lineNumber); + return this.modelLineProjections[info.modelLineNumber - 1].normalizePosition(this.model, info.modelLineNumber, info.modelLineWrappedLineIdx, position, affinity); } public getLineIndentColumn(lineNumber: number): number { - const viewLineNumber = this._toValidViewLineNumber(lineNumber); - const r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); - const lineIndex = r.index; - const remainder = r.remainder; - - if (remainder === 0) { - return this.model.getLineIndentColumn(lineIndex + 1); + const info = this.getViewLineInfo(lineNumber); + if (info.modelLineWrappedLineIdx === 0) { + return this.model.getLineIndentColumn(info.modelLineNumber); } // wrapped lines have no indentation. @@ -1171,9 +1056,12 @@ class ViewLineInfoGroupedByModelRange { } } -class VisibleIdentitySplitLine implements ISplitLine { +/** + * This projection does not change the model line. +*/ +class IdentityModelLineProjection implements IModelLineProjection { - public static readonly INSTANCE = new VisibleIdentitySplitLine(); + public static readonly INSTANCE = new IdentityModelLineProjection(); private constructor() { } @@ -1181,11 +1069,11 @@ class VisibleIdentitySplitLine implements ISplitLine { return true; } - public setVisible(isVisible: boolean): ISplitLine { + public setVisible(isVisible: boolean): IModelLineProjection { if (isVisible) { return this; } - return InvisibleIdentitySplitLine.INSTANCE; + return HiddenModelLineProjection.INSTANCE; } public getLineBreakData(): LineBreakData | null { @@ -1255,9 +1143,12 @@ class VisibleIdentitySplitLine implements ISplitLine { } } -class InvisibleIdentitySplitLine implements ISplitLine { +/** + * This projection hides the model line. + */ +class HiddenModelLineProjection implements IModelLineProjection { - public static readonly INSTANCE = new InvisibleIdentitySplitLine(); + public static readonly INSTANCE = new HiddenModelLineProjection(); private constructor() { } @@ -1265,11 +1156,11 @@ class InvisibleIdentitySplitLine implements ISplitLine { return false; } - public setVisible(isVisible: boolean): ISplitLine { + public setVisible(isVisible: boolean): IModelLineProjection { if (!isVisible) { return this; } - return VisibleIdentitySplitLine.INSTANCE; + return IdentityModelLineProjection.INSTANCE; } public getLineBreakData(): LineBreakData | null { @@ -1325,7 +1216,12 @@ class InvisibleIdentitySplitLine implements ISplitLine { } } -export class SplitLine implements ISplitLine { +/** + * This projection is used to + * * wrap model lines + * * inject text + */ +export class ModelLineProjection implements IModelLineProjection { private readonly _lineBreakData: LineBreakData; private _isVisible: boolean; @@ -1339,7 +1235,7 @@ export class SplitLine implements ISplitLine { return this._isVisible; } - public setVisible(isVisible: boolean): ISplitLine { + public setVisible(isVisible: boolean): IModelLineProjection { this._isVisible = isVisible; return this; } @@ -1626,15 +1522,15 @@ function _makeSpaces(count: number): string { return new Array(count + 1).join(' '); } -function createSplitLine(lineBreakData: LineBreakData | null, isVisible: boolean): ISplitLine { +function createModelLineProjection(lineBreakData: LineBreakData | null, isVisible: boolean): IModelLineProjection { if (lineBreakData === null) { // No mapping needed if (isVisible) { - return VisibleIdentitySplitLine.INSTANCE; + return IdentityModelLineProjection.INSTANCE; } - return InvisibleIdentitySplitLine.INSTANCE; + return HiddenModelLineProjection.INSTANCE; } else { - return new SplitLine(lineBreakData, isVisible); + return new ModelLineProjection(lineBreakData, isVisible); } } diff --git a/src/vs/editor/contrib/bracketMatching/bracketMatching.ts b/src/vs/editor/contrib/bracketMatching/bracketMatching.ts index 955bfa89dfd..7dbc428c99e 100644 --- a/src/vs/editor/contrib/bracketMatching/bracketMatching.ts +++ b/src/vs/editor/contrib/bracketMatching/bracketMatching.ts @@ -181,7 +181,7 @@ export class BracketMatchingController extends Disposable implements IEditorCont const position = selection.getStartPosition(); // find matching brackets if position is on a bracket - const brackets = model.matchBracket(position); + const brackets = model.bracketPairs.matchBracket(position); let newCursorPosition: Position | null = null; if (brackets) { if (brackets[0].containsPosition(position)) { @@ -191,12 +191,12 @@ export class BracketMatchingController extends Disposable implements IEditorCont } } else { // find the enclosing brackets if the position isn't on a matching bracket - const enclosingBrackets = model.findEnclosingBrackets(position); + const enclosingBrackets = model.bracketPairs.findEnclosingBrackets(position); if (enclosingBrackets) { newCursorPosition = enclosingBrackets[0].getStartPosition(); } else { // no enclosing brackets, try the very first next bracket - const nextBracket = model.findNextBracket(position); + const nextBracket = model.bracketPairs.findNextBracket(position); if (nextBracket && nextBracket.range) { newCursorPosition = nextBracket.range.getStartPosition(); } @@ -223,14 +223,14 @@ export class BracketMatchingController extends Disposable implements IEditorCont this._editor.getSelections().forEach(selection => { const position = selection.getStartPosition(); - let brackets = model.matchBracket(position); + let brackets = model.bracketPairs.matchBracket(position); if (!brackets) { - brackets = model.findEnclosingBrackets(position); + brackets = model.bracketPairs.findEnclosingBrackets(position); if (!brackets) { - const nextBracket = model.findNextBracket(position); + const nextBracket = model.bracketPairs.findNextBracket(position); if (nextBracket && nextBracket.range) { - brackets = model.matchBracket(nextBracket.range.getStartPosition()); + brackets = model.bracketPairs.matchBracket(nextBracket.range.getStartPosition()); } } } @@ -348,10 +348,10 @@ export class BracketMatchingController extends Disposable implements IEditorCont if (previousIndex < previousLen && previousData[previousIndex].position.equals(position)) { newData[newDataLen++] = previousData[previousIndex]; } else { - let brackets = model.matchBracket(position); + let brackets = model.bracketPairs.matchBracket(position); let options = BracketMatchingController._DECORATION_OPTIONS_WITH_OVERVIEW_RULER; if (!brackets && this._matchBrackets === 'always') { - brackets = model.findEnclosingBrackets(position, 20 /* give at most 20ms to compute */); + brackets = model.bracketPairs.findEnclosingBrackets(position, 20 /* give at most 20ms to compute */); options = BracketMatchingController._DECORATION_OPTIONS_WITHOUT_OVERVIEW_RULER; } newData[newDataLen++] = new BracketsData(position, brackets, options); diff --git a/src/vs/editor/contrib/colorPicker/colorPickerWidget.ts b/src/vs/editor/contrib/colorPicker/colorPickerWidget.ts index 85d889c652f..6c21e05e257 100644 --- a/src/vs/editor/contrib/colorPicker/colorPickerWidget.ts +++ b/src/vs/editor/contrib/colorPicker/colorPickerWidget.ts @@ -52,6 +52,8 @@ export class ColorPickerHeader extends Disposable { this._register(model.onDidChangePresentation(this.onDidChangePresentation, this)); this.pickedColorNode.style.backgroundColor = Color.Format.CSS.format(model.color) || ''; this.pickedColorNode.classList.toggle('light', model.color.rgba.a < 0.5 ? this.backgroundColor.isLighter() : model.color.isLighter()); + + this.onDidChangeColor(this.model.color); } private onDidChangeColor(color: Color): void { diff --git a/src/vs/editor/contrib/gotoError/markerNavigationService.ts b/src/vs/editor/contrib/gotoError/markerNavigationService.ts index 04a59413ed3..d9213a66ce6 100644 --- a/src/vs/editor/contrib/gotoError/markerNavigationService.ts +++ b/src/vs/editor/contrib/gotoError/markerNavigationService.ts @@ -15,6 +15,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IMarker, IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export class MarkerCoordinate { constructor( @@ -38,6 +39,7 @@ export class MarkerList { constructor( resourceFilter: URI | ((uri: URI) => boolean) | undefined, @IMarkerService private readonly _markerService: IMarkerService, + @IConfigurationService private readonly _configService: IConfigurationService, ) { if (URI.isUri(resourceFilter)) { this._resourceFilter = uri => uri.toString() === resourceFilter.toString(); @@ -45,6 +47,17 @@ export class MarkerList { this._resourceFilter = resourceFilter; } + const compareOrder = this._configService.getValue<string>('problems.compareOrder'); + const compareMarker = (a: IMarker, b: IMarker): number => { + let res = compare(a.resource.toString(), b.resource.toString()); + if (compareOrder === 'position') { + res = Range.compareRangesUsingStarts(a, b) || MarkerSeverity.compare(a.severity, b.severity); + } else { + res = MarkerSeverity.compare(a.severity, b.severity) || Range.compareRangesUsingStarts(a, b); + } + return res; + }; + const updateMarker = () => { this._markers = this._markerService.read({ resource: URI.isUri(resourceFilter) ? resourceFilter : undefined, @@ -53,7 +66,7 @@ export class MarkerList { if (typeof resourceFilter === 'function') { this._markers = this._markers.filter(m => this._resourceFilter!(m.resource)); } - this._markers.sort(MarkerList._compareMarker); + this._markers.sort(compareMarker); }; updateMarker(); @@ -164,17 +177,6 @@ export class MarkerList { } return undefined; } - - private static _compareMarker(a: IMarker, b: IMarker): number { - let res = compare(a.resource.toString(), b.resource.toString()); - if (res === 0) { - res = MarkerSeverity.compare(a.severity, b.severity); - } - if (res === 0) { - res = Range.compareRangesUsingStarts(a, b); - } - return res; - } } export const IMarkerNavigationService = createDecorator<IMarkerNavigationService>('IMarkerNavigationService'); @@ -195,7 +197,10 @@ class MarkerNavigationService implements IMarkerNavigationService, IMarkerListPr private readonly _provider = new LinkedList<IMarkerListProvider>(); - constructor(@IMarkerService private readonly _markerService: IMarkerService) { } + constructor( + @IMarkerService private readonly _markerService: IMarkerService, + @IConfigurationService private readonly _configService: IConfigurationService, + ) { } registerProvider(provider: IMarkerListProvider): IDisposable { const remove = this._provider.unshift(provider); @@ -210,7 +215,7 @@ class MarkerNavigationService implements IMarkerNavigationService, IMarkerListPr } } // default - return new MarkerList(resource, this._markerService); + return new MarkerList(resource, this._markerService, this._configService); } } diff --git a/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts b/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts index e7da40052c5..655395a7aba 100644 --- a/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts +++ b/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts @@ -18,7 +18,8 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { IFoundBracket, IModelDeltaDecoration, ITextModel, IWordAtPosition } from 'vs/editor/common/model'; +import { IModelDeltaDecoration, ITextModel, IWordAtPosition } from 'vs/editor/common/model'; +import { IFoundBracket } from 'vs/editor/common/model/bracketPairs/bracketPairs'; import { DefinitionProviderRegistry, LocationLink } from 'vs/editor/common/modes'; import { IModeService } from 'vs/editor/common/services/modeService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -260,7 +261,7 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri const brackets: IFoundBracket[] = []; let ignoreFirstEmpty = true; - let currentBracket = textEditorModel.findNextBracket(new Position(startLineNumber, 1)); + let currentBracket = textEditorModel.bracketPairs.findNextBracket(new Position(startLineNumber, 1)); while (currentBracket !== null) { if (brackets.length === 0) { @@ -294,7 +295,7 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri return new Range(startLineNumber, 1, maxLineNumber + 1, 1); } - currentBracket = textEditorModel.findNextBracket(new Position(nextLineNumber, nextColumn)); + currentBracket = textEditorModel.bracketPairs.findNextBracket(new Position(nextLineNumber, nextColumn)); } return new Range(startLineNumber, 1, maxLineNumber + 1, 1); diff --git a/src/vs/editor/contrib/inlineCompletions/ghostTextWidget.ts b/src/vs/editor/contrib/inlineCompletions/ghostTextWidget.ts index 84d0d7cec65..4fe8147dcb3 100644 --- a/src/vs/editor/contrib/inlineCompletions/ghostTextWidget.ts +++ b/src/vs/editor/contrib/inlineCompletions/ghostTextWidget.ts @@ -21,7 +21,7 @@ import { IDecorationRenderOptions } from 'vs/editor/common/editorCommon'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; import { ILanguageIdCodec } from 'vs/editor/common/modes'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { ghostTextBorder, ghostTextForeground } from 'vs/editor/common/view/editorColorRegistry'; +import { ghostTextBackground, ghostTextBorder, ghostTextForeground } from 'vs/editor/common/view/editorColorRegistry'; import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations'; import { RenderLineInput, renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer'; import { InlineDecorationType } from 'vs/editor/common/viewModel/viewModel'; @@ -226,6 +226,7 @@ class DecorationsWidget implements IDisposable { const colorTheme = this.themeService.getColorTheme(); const foreground = colorTheme.getColor(ghostTextForeground); + let opacity: string | undefined = undefined; let color: string | undefined = undefined; if (foreground) { @@ -499,15 +500,18 @@ class ViewMoreLinesContentWidget extends Disposable implements IContentWidget { registerThemingParticipant((theme, collector) => { const foreground = theme.getColor(ghostTextForeground); - if (foreground) { - const opacity = String(foreground.rgba.a); - const color = Color.Format.CSS.format(opaque(foreground))!; - // `!important` ensures that other decorations don't cause a style conflict (#132017). - collector.addRule(`.monaco-editor .ghost-text-decoration { opacity: ${opacity} !important; color: ${color} !important; }`); + collector.addRule(`.monaco-editor .ghost-text-decoration { color: ${foreground.toString()} !important; }`); collector.addRule(`.monaco-editor .ghost-text-decoration-preview { color: ${foreground.toString()} !important; }`); - collector.addRule(`.monaco-editor .suggest-preview-text .ghost-text { opacity: ${opacity} !important; color: ${color} !important; }`); + collector.addRule(`.monaco-editor .suggest-preview-text .ghost-text { color: ${foreground.toString()} !important; }`); + } + + const background = theme.getColor(ghostTextBackground); + if (background) { + collector.addRule(`.monaco-editor .ghost-text-decoration { background-color: ${background.toString()}; }`); + collector.addRule(`.monaco-editor .ghost-text-decoration-preview { background-color: ${background.toString()}; }`); + collector.addRule(`.monaco-editor .suggest-preview-text .ghost-text { background-color: ${background.toString()}; }`); } const border = theme.getColor(ghostTextBorder); diff --git a/src/vs/editor/contrib/message/messageController.css b/src/vs/editor/contrib/message/messageController.css index 924349d112e..fdd3476fbd3 100644 --- a/src/vs/editor/contrib/message/messageController.css +++ b/src/vs/editor/contrib/message/messageController.css @@ -32,6 +32,13 @@ .monaco-editor .monaco-editor-overlaymessage .message { padding: 1px 4px; + color: var(--vscode-inputValidation-infoForeground); + background-color: var(--vscode-inputValidation-infoBackground); + border: 1px solid var(--vscode-inputValidation-infoBorder); +} + +.monaco-editor.hc-black .monaco-editor-overlaymessage .message { + border-width: 2px; } .monaco-editor .monaco-editor-overlaymessage .anchor { @@ -44,6 +51,14 @@ position: absolute; } +.monaco-editor .monaco-editor-overlaymessage .anchor.top { + border-bottom-color: var(--vscode-inputValidation-infoBorder); +} + +.monaco-editor .monaco-editor-overlaymessage .anchor.below { + border-top-color: var(--vscode-inputValidation-infoBorder); +} + .monaco-editor .monaco-editor-overlaymessage:not(.below) .anchor.top, .monaco-editor .monaco-editor-overlaymessage.below .anchor.below { display: none; diff --git a/src/vs/editor/contrib/message/messageController.ts b/src/vs/editor/contrib/message/messageController.ts index 277309eaed7..5efbb4e9be0 100644 --- a/src/vs/editor/contrib/message/messageController.ts +++ b/src/vs/editor/contrib/message/messageController.ts @@ -16,9 +16,6 @@ import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; import * as nls from 'vs/nls'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground } from 'vs/platform/theme/common/colorRegistry'; -import { ColorScheme } from 'vs/platform/theme/common/theme'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; export class MessageController implements IEditorContribution { @@ -193,21 +190,3 @@ class MessageWidget implements IContentWidget { } registerEditorContribution(MessageController.ID, MessageController); - -registerThemingParticipant((theme, collector) => { - const border = theme.getColor(inputValidationInfoBorder); - if (border) { - let borderWidth = theme.type === ColorScheme.HIGH_CONTRAST ? 2 : 1; - collector.addRule(`.monaco-editor .monaco-editor-overlaymessage .anchor.below { border-top-color: ${border}; }`); - collector.addRule(`.monaco-editor .monaco-editor-overlaymessage .anchor.top { border-bottom-color: ${border}; }`); - collector.addRule(`.monaco-editor .monaco-editor-overlaymessage .message { border: ${borderWidth}px solid ${border}; }`); - } - const background = theme.getColor(inputValidationInfoBackground); - if (background) { - collector.addRule(`.monaco-editor .monaco-editor-overlaymessage .message { background-color: ${background}; }`); - } - const foreground = theme.getColor(inputValidationInfoForeground); - if (foreground) { - collector.addRule(`.monaco-editor .monaco-editor-overlaymessage .message { color: ${foreground}; }`); - } -}); diff --git a/src/vs/editor/contrib/smartSelect/bracketSelections.ts b/src/vs/editor/contrib/smartSelect/bracketSelections.ts index fc4e5e43337..716d830d06a 100644 --- a/src/vs/editor/contrib/smartSelect/bracketSelections.ts +++ b/src/vs/editor/contrib/smartSelect/bracketSelections.ts @@ -41,7 +41,7 @@ export class BracketSelectionRangeProvider implements SelectionRangeProvider { resolve(); break; } - let bracket = model.findNextBracket(pos); + let bracket = model.bracketPairs.findNextBracket(pos); if (!bracket) { resolve(); break; @@ -86,7 +86,7 @@ export class BracketSelectionRangeProvider implements SelectionRangeProvider { resolve(); break; } - let bracket = model.findPrevBracket(pos); + let bracket = model.bracketPairs.findPrevBracket(pos); if (!bracket) { resolve(); break; diff --git a/src/vs/editor/contrib/snippet/snippetSession.css b/src/vs/editor/contrib/snippet/snippetSession.css index d262eaf1b11..0f52c3d5943 100644 --- a/src/vs/editor/contrib/snippet/snippetSession.css +++ b/src/vs/editor/contrib/snippet/snippetSession.css @@ -7,9 +7,13 @@ min-width: 2px; outline-style: solid; outline-width: 1px; + background-color: var(--vscode-editor-snippetTabstopHighlightBackground, transparent); + outline-color: var(--vscode-editor-snippetTabstopHighlightBorder, transparent); } .monaco-editor .finish-snippet-placeholder { outline-style: solid; outline-width: 1px; + background-color: var(--vscode-editor-snippetFinalTabstopHighlightBackground, transparent); + outline-color: var(--vscode-editor-snippetFinalTabstopHighlightBorder, transparent); } diff --git a/src/vs/editor/contrib/snippet/snippetSession.ts b/src/vs/editor/contrib/snippet/snippetSession.ts index 5b484ffb7bf..ff99f621a7a 100644 --- a/src/vs/editor/contrib/snippet/snippetSession.ts +++ b/src/vs/editor/contrib/snippet/snippetSession.ts @@ -19,23 +19,10 @@ import { IIdentifiedSingleEditOperation, ITextModel, TrackedRangeStickiness } fr import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { OvertypingCapturer } from 'vs/editor/contrib/suggest/suggestOvertypingCapturer'; import { ILabelService } from 'vs/platform/label/common/label'; -import * as colors from 'vs/platform/theme/common/colorRegistry'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { Choice, Marker, Placeholder, SnippetParser, Text, TextmateSnippet } from './snippetParser'; import { ClipboardBasedVariableResolver, CommentBasedVariableResolver, CompositeSnippetVariableResolver, ModelBasedVariableResolver, RandomBasedVariableResolver, SelectionBasedVariableResolver, TimeBasedVariableResolver, WorkspaceBasedVariableResolver } from './snippetVariables'; -registerThemingParticipant((theme, collector) => { - - function getColorGraceful(name: string) { - const color = theme.getColor(name); - return color ? color.toString() : 'transparent'; - } - - collector.addRule(`.monaco-editor .snippet-placeholder { background-color: ${getColorGraceful(colors.snippetTabstopHighlightBackground)}; outline-color: ${getColorGraceful(colors.snippetTabstopHighlightBorder)}; }`); - collector.addRule(`.monaco-editor .finish-snippet-placeholder { background-color: ${getColorGraceful(colors.snippetFinalTabstopHighlightBackground)}; outline-color: ${getColorGraceful(colors.snippetFinalTabstopHighlightBorder)}; }`); -}); - export class OneSnippet { private _placeholderDecorations?: Map<Placeholder, string>; diff --git a/src/vs/editor/contrib/suggest/media/suggest.css b/src/vs/editor/contrib/suggest/media/suggest.css index 3d118cc8d09..ae438dbf51d 100644 --- a/src/vs/editor/contrib/suggest/media/suggest.css +++ b/src/vs/editor/contrib/suggest/media/suggest.css @@ -23,6 +23,8 @@ width: 100%; border-style: solid; border-width: 1px; + border-color: var(--vscode-editorSuggestWidget-border); + background-color: var(--vscode-editorSuggestWidget-background); } .monaco-editor.hc-black .suggest-widget, @@ -41,7 +43,7 @@ width: 100%; font-size: 80%; padding: 0 4px 0 4px; - border-top: 1px solid transparent; + border-top: 1px solid var(--vscode-editorSuggestWidget-border); overflow: hidden; } @@ -109,6 +111,14 @@ touch-action: none; } +.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused { + color: var(--vscode-editorSuggestWidget-selectedForeground); +} + +.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused .codicon { + color: var(--vscode-editorSuggestWidget-selectedIconForeground); +} + .monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents { flex: 1; height: 100%; @@ -132,6 +142,14 @@ font-weight: bold; } +.monaco-editor .suggest-widget .monaco-list .monaco-list-row .monaco-highlighted-label .highlight { + color: var(--vscode-editorSuggestWidget-highlightForeground); +} + +.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused .monaco-highlighted-label .highlight { + color: var(--vscode-editorSuggestWidget-focusHighlightForeground); +} + /** ReadMore Icon styles **/ .monaco-editor .suggest-details>.monaco-scrollable-element>.body>.header>.codicon-close, @@ -329,6 +347,23 @@ display: flex; flex-direction: column; cursor: default; + color: var(--vscode-editorSuggestWidget-foreground); +} + +.monaco-editor .suggest-details.focused { + border-color: var(--vscode-focusBorder); +} + +.monaco-editor .suggest-details a { + color: var(--vscode-textLink-foreground); +} + +.monaco-editor .suggest-details a:hover { + color: var(--vscode-textLink-activeForeground); +} + +.monaco-editor .suggest-details code { + background-color: var(--vscode-textCodeBlock-background); } .monaco-editor .suggest-details.no-docs { diff --git a/src/vs/editor/contrib/suggest/suggestController.ts b/src/vs/editor/contrib/suggest/suggestController.ts index 8a7c2f559c3..6de2eb0900c 100644 --- a/src/vs/editor/contrib/suggest/suggestController.ts +++ b/src/vs/editor/contrib/suggest/suggestController.ts @@ -457,9 +457,9 @@ export class SuggestController implements IEditorContribution { } } - triggerSuggest(onlyFrom?: Set<CompletionItemProvider>): void { + triggerSuggest(onlyFrom?: Set<CompletionItemProvider>, auto?: boolean): void { if (this.editor.hasModel()) { - this.model.trigger({ auto: false, shy: false }, false, onlyFrom); + this.model.trigger({ auto: auto ?? false, shy: false }, false, onlyFrom); this.editor.revealPosition(this.editor.getPosition(), ScrollType.Smooth); this.editor.focus(); } @@ -662,14 +662,22 @@ export class TriggerSuggestAction extends EditorAction { }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + run(_accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { const controller = SuggestController.get(editor); if (!controller) { return; } - controller.triggerSuggest(); + type TriggerArgs = { auto: boolean }; + let auto: boolean | undefined; + if (args && typeof args === 'object') { + if ((<TriggerArgs>args).auto === true) { + auto = true; + } + } + + controller.triggerSuggest(undefined, auto); } } diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index ffff62a951a..f2bfb4cfe69 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -25,9 +25,9 @@ import * as nls from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { activeContrastBorder, editorForeground, editorWidgetBackground, editorWidgetBorder, focusBorder, listFocusHighlightForeground, listHighlightForeground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, registerColor, textCodeBlockBackground, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; +import { activeContrastBorder, editorForeground, editorWidgetBackground, editorWidgetBorder, listFocusHighlightForeground, listHighlightForeground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, registerColor } from 'vs/platform/theme/common/colorRegistry'; import { attachListStyler } from 'vs/platform/theme/common/styler'; -import { IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { CompletionModel } from './completionModel'; import { ResizableHTMLElement } from './resizable'; import { CompletionItem, Context as SuggestContext } from './suggest'; @@ -140,9 +140,6 @@ export class SuggestWidget implements IDisposable { private readonly _onDetailsKeydown = new Emitter<IKeyboardEvent>(); readonly onDetailsKeyDown: Event<IKeyboardEvent> = this._onDetailsKeydown.event; - private _detailsFocusBorderColor?: string; - private _detailsBorderColor?: string; - constructor( private readonly editor: ICodeEditor, @IStorageService private readonly _storageService: IStorageService, @@ -336,24 +333,6 @@ export class SuggestWidget implements IDisposable { } private _onThemeChange(theme: IColorTheme) { - const backgroundColor = theme.getColor(editorSuggestWidgetBackground); - if (backgroundColor) { - this.element.domNode.style.backgroundColor = backgroundColor.toString(); - this._messageElement.style.backgroundColor = backgroundColor.toString(); - this._details.widget.domNode.style.backgroundColor = backgroundColor.toString(); - } - const borderColor = theme.getColor(editorSuggestWidgetBorder); - if (borderColor) { - this.element.domNode.style.borderColor = borderColor.toString(); - this._messageElement.style.borderColor = borderColor.toString(); - this._status.element.style.borderTopColor = borderColor.toString(); - this._details.widget.domNode.style.borderColor = borderColor.toString(); - this._detailsBorderColor = borderColor.toString(); - } - const focusBorderColor = theme.getColor(focusBorder); - if (focusBorderColor) { - this._detailsFocusBorderColor = focusBorderColor.toString(); - } this._details.widget.borderWidth = theme.type === 'hc' ? 2 : 1; } @@ -547,9 +526,7 @@ export class SuggestWidget implements IDisposable { this._layout(this.element.size); // Reset focus border - if (this._detailsBorderColor) { - this._details.widget.domNode.style.borderColor = this._detailsBorderColor; - } + this._details.widget.domNode.classList.remove('focused'); } selectNextPage(): boolean { @@ -655,14 +632,11 @@ export class SuggestWidget implements IDisposable { toggleDetailsFocus(): void { if (this._state === State.Details) { this._setState(State.Open); - if (this._detailsBorderColor) { - this._details.widget.domNode.style.borderColor = this._detailsBorderColor; - } + this._details.widget.domNode.classList.remove('focused'); + } else if (this._state === State.Open && this._isDetailsVisible()) { this._setState(State.Details); - if (this._detailsFocusBorderColor) { - this._details.widget.domNode.style.borderColor = this._detailsFocusBorderColor; - } + this._details.widget.domNode.classList.add('focused'); } } @@ -982,45 +956,3 @@ export class SuggestContentWidget implements IContentWidget { this._position = position; } } - -registerThemingParticipant((theme, collector) => { - const matchHighlight = theme.getColor(editorSuggestWidgetHighlightForeground); - if (matchHighlight) { - collector.addRule(`.monaco-editor .suggest-widget .monaco-list .monaco-list-row .monaco-highlighted-label .highlight { color: ${matchHighlight}; }`); - } - - const matchHighlightFocus = theme.getColor(editorSuggestWidgetHighlightFocusForeground); - if (matchHighlight) { - collector.addRule(`.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused .monaco-highlighted-label .highlight { color: ${matchHighlightFocus}; }`); - } - - const foreground = theme.getColor(editorSuggestWidgetForeground); - if (foreground) { - collector.addRule(`.monaco-editor .suggest-widget, .monaco-editor .suggest-details { color: ${foreground}; }`); - } - - const selectedForeground = theme.getColor(editorSuggestWidgetSelectedForeground); - if (selectedForeground) { - collector.addRule(`.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused { color: ${selectedForeground}; }`); - } - - const selectedIconForeground = theme.getColor(editorSuggestWidgetSelectedIconForeground); - if (selectedIconForeground) { - collector.addRule(`.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused .codicon { color: ${selectedIconForeground}; }`); - } - - const link = theme.getColor(textLinkForeground); - if (link) { - collector.addRule(`.monaco-editor .suggest-details a { color: ${link}; }`); - } - - const linkHover = theme.getColor(textLinkActiveForeground); - if (linkHover) { - collector.addRule(`.monaco-editor .suggest-details a:hover { color: ${linkHover}; }`); - } - - const codeBackground = theme.getColor(textCodeBlockBackground); - if (codeBackground) { - collector.addRule(`.monaco-editor .suggest-details code { background-color: ${codeBackground}; }`); - } -}); diff --git a/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts b/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts index ece509fa0d0..779f91ebf31 100644 --- a/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts +++ b/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts @@ -12,7 +12,7 @@ import { BuiltinTheme, IStandaloneTheme, IStandaloneThemeData, IStandaloneThemeS import { hc_black, vs, vs_dark } from 'vs/editor/standalone/common/themes'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ColorIdentifier, Extensions, IColorRegistry } from 'vs/platform/theme/common/colorRegistry'; +import { asCssVariableName, ColorIdentifier, Extensions, IColorRegistry } from 'vs/platform/theme/common/colorRegistry'; import { Extensions as ThemingExtensions, ICssStyleCollector, IFileIconTheme, IThemingRegistry, ITokenStyle } from 'vs/platform/theme/common/themeService'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { ColorScheme } from 'vs/platform/theme/common/theme'; @@ -346,6 +346,15 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon }; themingRegistry.getThemingParticipants().forEach(p => p(this._theme, ruleCollector, this._environment)); + const colorVariables: string[] = []; + for (const item of colorRegistry.getColors()) { + const color = this._theme.getColor(item.id, true); + if (color) { + colorVariables.push(`${asCssVariableName(item.id)}: ${color.toString()};`); + } + } + ruleCollector.addRule(`.monaco-editor { ${colorVariables.join('\n')} }`); + const colorMap = this._colorMapOverride || this._theme.tokenTheme.getColorMap(); ruleCollector.addRule(generateTokensCSSForColorMap(colorMap)); diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts index efc635346ac..b21d9f3e204 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts @@ -7,8 +7,8 @@ import assert = require('assert'); import { splitLines } from 'vs/base/common/strings'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; -import { BeforeEditPositionMapper, TextEditInfo } from 'vs/editor/common/model/bracketPairs/impl/beforeEditPositionMapper'; -import { Length, lengthOfString, lengthToObj, lengthToPosition, toLength } from 'vs/editor/common/model/bracketPairs/impl/length'; +import { BeforeEditPositionMapper, TextEditInfo } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/beforeEditPositionMapper'; +import { Length, lengthOfString, lengthToObj, lengthToPosition, toLength } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/length'; suite('Bracket Pair Colorizer - BeforeEditPositionMapper', () => { test('Single-Line 1', () => { diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts index 2f2ed1c5825..5d47baa9122 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts @@ -5,9 +5,9 @@ import assert = require('assert'); import { DisposableStore } from 'vs/base/common/lifecycle'; -import { LanguageAgnosticBracketTokens } from 'vs/editor/common/model/bracketPairs/impl/brackets'; -import { SmallImmutableSet, DenseKeyProvider } from 'vs/editor/common/model/bracketPairs/impl/smallImmutableSet'; -import { Token, TokenKind } from 'vs/editor/common/model/bracketPairs/impl/tokenizer'; +import { LanguageAgnosticBracketTokens } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/brackets'; +import { SmallImmutableSet, DenseKeyProvider } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/smallImmutableSet'; +import { Token, TokenKind } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/tokenizer'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/concat23Trees.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/concat23Trees.test.ts index b75bb1fb05e..ca39ae3f3e3 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/concat23Trees.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/concat23Trees.test.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import assert = require('assert'); -import { AstNode, AstNodeKind, ListAstNode, TextAstNode } from 'vs/editor/common/model/bracketPairs/impl/ast'; -import { toLength } from 'vs/editor/common/model/bracketPairs/impl/length'; -import { concat23Trees } from 'vs/editor/common/model/bracketPairs/impl/concat23Trees'; +import { AstNode, AstNodeKind, ListAstNode, TextAstNode } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/ast'; +import { toLength } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/length'; +import { concat23Trees } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/concat23Trees'; suite('Bracket Pair Colorizer - mergeItems', () => { test('Clone', () => { diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/length.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/length.test.ts index 487564aba52..75a55bfaf47 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/length.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/length.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert = require('assert'); -import { Length, lengthAdd, lengthDiffNonNegative, lengthToObj, toLength } from 'vs/editor/common/model/bracketPairs/impl/length'; +import { Length, lengthAdd, lengthDiffNonNegative, lengthToObj, toLength } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/length'; suite('Bracket Pair Colorizer - Length', () => { function toStr(length: Length): string { diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/smallImmutableSet.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/smallImmutableSet.test.ts index 7d91fe13013..f608e7da28b 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/smallImmutableSet.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/smallImmutableSet.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert = require('assert'); -import { DenseKeyProvider, SmallImmutableSet } from 'vs/editor/common/model/bracketPairs/impl/smallImmutableSet'; +import { DenseKeyProvider, SmallImmutableSet } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/smallImmutableSet'; suite('Bracket Pair Colorizer - ImmutableSet', () => { test('Basic', () => { diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts index a87b325d821..d15f4ed7bf5 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts @@ -5,10 +5,10 @@ import assert = require('assert'); import { TokenizationResult2 } from 'vs/editor/common/core/token'; -import { LanguageAgnosticBracketTokens } from 'vs/editor/common/model/bracketPairs/impl/brackets'; -import { Length, lengthAdd, lengthsToRange, lengthZero } from 'vs/editor/common/model/bracketPairs/impl/length'; -import { DenseKeyProvider } from 'vs/editor/common/model/bracketPairs/impl/smallImmutableSet'; -import { TextBufferTokenizer, Token, Tokenizer, TokenKind } from 'vs/editor/common/model/bracketPairs/impl/tokenizer'; +import { LanguageAgnosticBracketTokens } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/brackets'; +import { Length, lengthAdd, lengthsToRange, lengthZero } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/length'; +import { DenseKeyProvider } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/smallImmutableSet'; +import { TextBufferTokenizer, Token, Tokenizer, TokenKind } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/tokenizer'; import { TextModel } from 'vs/editor/common/model/textModel'; import { IState, ITokenizationSupport, LanguageId, MetadataConsts, StandardTokenType, TokenizationRegistry } from 'vs/editor/common/modes'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; diff --git a/src/vs/editor/test/common/model/textModelWithTokens.test.ts b/src/vs/editor/test/common/model/textModelWithTokens.test.ts index a6d453b500e..6c903ced8e3 100644 --- a/src/vs/editor/test/common/model/textModelWithTokens.test.ts +++ b/src/vs/editor/test/common/model/textModelWithTokens.test.ts @@ -8,7 +8,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { TokenizationResult2 } from 'vs/editor/common/core/token'; -import { IFoundBracket } from 'vs/editor/common/model'; +import { IFoundBracket } from 'vs/editor/common/model/bracketPairs/bracketPairs'; import { TextModel } from 'vs/editor/common/model/textModel'; import { ITokenizationSupport, MetadataConsts, TokenizationRegistry, StandardTokenType } from 'vs/editor/common/modes'; import { CharacterPair } from 'vs/editor/common/modes/languageConfiguration'; @@ -99,7 +99,7 @@ suite('TextModelWithTokens', () => { } } - let actual = model.findPrevBracket({ + let actual = model.bracketPairs.findPrevBracket({ lineNumber: lineNumber, column: column }); @@ -125,7 +125,7 @@ suite('TextModelWithTokens', () => { } } - let actual = model.findNextBracket({ + let actual = model.bracketPairs.findNextBracket({ lineNumber: lineNumber, column: column }); @@ -150,12 +150,12 @@ suite('TextModelWithTokens', () => { }); function assertIsNotBracket(model: TextModel, lineNumber: number, column: number) { - const match = model.matchBracket(new Position(lineNumber, column)); + const match = model.bracketPairs.matchBracket(new Position(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); + const actual = model.bracketPairs.matchBracket(testPosition); assert.deepStrictEqual(actual, expected, 'matches brackets at ' + testPosition); } @@ -442,7 +442,7 @@ suite('TextModelWithTokens', () => { model.forceTokenization(2); model.forceTokenization(3); - assert.deepStrictEqual(model.matchBracket(new Position(2, 14)), [new Range(2, 13, 2, 14), new Range(2, 18, 2, 19)]); + assert.deepStrictEqual(model.bracketPairs.matchBracket(new Position(2, 14)), [new Range(2, 13, 2, 14), new Range(2, 18, 2, 19)]); disposables.dispose(); }); @@ -520,8 +520,8 @@ suite('TextModelWithTokens', () => { model.forceTokenization(2); model.forceTokenization(3); - assert.deepStrictEqual(model.matchBracket(new Position(2, 23)), null); - assert.deepStrictEqual(model.matchBracket(new Position(2, 20)), null); + assert.deepStrictEqual(model.bracketPairs.matchBracket(new Position(2, 23)), null); + assert.deepStrictEqual(model.bracketPairs.matchBracket(new Position(2, 20)), null); disposables.dispose(); }); @@ -630,7 +630,7 @@ suite('TextModelWithTokens regression tests', () => { 'End Module', ].join('\n'), undefined, languageId)); - const actual = model.matchBracket(new Position(4, 1)); + const actual = model.bracketPairs.matchBracket(new Position(4, 1)); assert.deepStrictEqual(actual, [new Range(4, 1, 4, 7), new Range(9, 1, 9, 11)]); disposables.dispose(); @@ -655,7 +655,7 @@ suite('TextModelWithTokens regression tests', () => { 'endsequence', ].join('\n'), undefined, languageId)); - const actual = model.matchBracket(new Position(3, 9)); + const actual = model.bracketPairs.matchBracket(new Position(3, 9)); assert.deepStrictEqual(actual, [new Range(3, 6, 3, 17), new Range(2, 6, 2, 14)]); disposables.dispose(); diff --git a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts index 0ac5ed2672d..f8dfc3eceec 100644 --- a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts @@ -58,7 +58,7 @@ suite('Editor ViewModel - PrefixSumComputer', () => { assert.strictEqual(indexOfResult.remainder, 3); // [1, 2, 2, 1, 3] - psc.changeValue(1, 2); + psc.setValue(1, 2); assert.strictEqual(psc.getTotalSum(), 9); assert.strictEqual(psc.getPrefixSum(0), 1); assert.strictEqual(psc.getPrefixSum(1), 3); @@ -67,7 +67,7 @@ suite('Editor ViewModel - PrefixSumComputer', () => { assert.strictEqual(psc.getPrefixSum(4), 9); // [1, 0, 2, 1, 3] - psc.changeValue(1, 0); + psc.setValue(1, 0); assert.strictEqual(psc.getTotalSum(), 7); assert.strictEqual(psc.getPrefixSum(0), 1); assert.strictEqual(psc.getPrefixSum(1), 1); @@ -100,7 +100,7 @@ suite('Editor ViewModel - PrefixSumComputer', () => { assert.strictEqual(indexOfResult.remainder, 3); // [1, 0, 0, 1, 3] - psc.changeValue(2, 0); + psc.setValue(2, 0); assert.strictEqual(psc.getTotalSum(), 5); assert.strictEqual(psc.getPrefixSum(0), 1); assert.strictEqual(psc.getPrefixSum(1), 1); @@ -127,7 +127,7 @@ suite('Editor ViewModel - PrefixSumComputer', () => { assert.strictEqual(indexOfResult.remainder, 3); // [1, 0, 0, 0, 3] - psc.changeValue(3, 0); + psc.setValue(3, 0); assert.strictEqual(psc.getTotalSum(), 4); assert.strictEqual(psc.getPrefixSum(0), 1); assert.strictEqual(psc.getPrefixSum(1), 1); @@ -151,9 +151,9 @@ suite('Editor ViewModel - PrefixSumComputer', () => { assert.strictEqual(indexOfResult.remainder, 3); // [1, 1, 0, 1, 1] - psc.changeValue(1, 1); - psc.changeValue(3, 1); - psc.changeValue(4, 1); + psc.setValue(1, 1); + psc.setValue(3, 1); + psc.setValue(4, 1); assert.strictEqual(psc.getTotalSum(), 4); assert.strictEqual(psc.getPrefixSum(0), 1); assert.strictEqual(psc.getPrefixSum(1), 2); diff --git a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts index a65305637ab..ab25a2773aa 100644 --- a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts +++ b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts @@ -14,7 +14,7 @@ import { TextModel } from 'vs/editor/common/model/textModel'; import * as modes from 'vs/editor/common/modes'; import { NULL_STATE } from 'vs/editor/common/modes/nullMode'; import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; -import { ISimpleModel, SplitLine, SplitLinesCollection } from 'vs/editor/common/viewModel/splitLinesCollection'; +import { ISimpleModel, ModelLineProjection, SplitLinesCollection } from 'vs/editor/common/viewModel/splitLinesCollection'; import { LineBreakData, ViewLineData } from 'vs/editor/common/viewModel/viewModel'; import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -946,8 +946,8 @@ function pos(lineNumber: number, column: number): Position { return new Position(lineNumber, column); } -function createSplitLine(splitLengths: number[], breakingOffsetsVisibleColumn: number[], wrappedTextIndentWidth: number, isVisible: boolean = true): SplitLine { - return new SplitLine(createLineBreakData(splitLengths, breakingOffsetsVisibleColumn, wrappedTextIndentWidth), isVisible); +function createSplitLine(splitLengths: number[], breakingOffsetsVisibleColumn: number[], wrappedTextIndentWidth: number, isVisible: boolean = true): ModelLineProjection { + return new ModelLineProjection(createLineBreakData(splitLengths, breakingOffsetsVisibleColumn, wrappedTextIndentWidth), isVisible); } function createLineBreakData(breakingLengths: number[], breakingOffsetsVisibleColumn: number[], wrappedTextIndentWidth: number): LineBreakData { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 42a1e515946..a4b0713389c 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -802,6 +802,10 @@ declare namespace monaco { * Get the position at `positionLineNumber` and `positionColumn`. */ getPosition(): Position; + /** + * Get the position at the start of the selection. + */ + getSelectionStart(): Position; /** * Create a new selection with a different `selectionStartLineNumber` and `selectionStartColumn`. */ @@ -810,6 +814,10 @@ declare namespace monaco { * Create a `Selection` from one or two positions */ static fromPositions(start: IPosition, end?: IPosition): Selection; + /** + * Creates a `Selection` from a range, given a direction. + */ + static fromRange(range: Range, direction: SelectionDirection): Selection; /** * Create a `Selection` from an `ISelection`. */ @@ -5817,6 +5825,11 @@ declare namespace monaco.languages { InsertAsSnippet = 4 } + export interface CompletionItemRanges { + insert: IRange; + replace: IRange; + } + /** * A completion item represents a text snippet that is * proposed to complete text that is being typed. @@ -5885,10 +5898,7 @@ declare namespace monaco.languages { * *Note:* The range must be a {@link Range.isSingleLine single line} and it must * {@link Range.contains contain} the position at which completion has been {@link CompletionItemProvider.provideCompletionItems requested}. */ - range: IRange | { - insert: IRange; - replace: IRange; - }; + range: IRange | CompletionItemRanges; /** * An optional set of characters that when pressed while this completion is active will accept it first and * then type that character. *Note* that all commit characters should have `length=1` and that superfluous diff --git a/src/vs/platform/assignment/common/assignment.ts b/src/vs/platform/assignment/common/assignment.ts new file mode 100644 index 00000000000..513296737f0 --- /dev/null +++ b/src/vs/platform/assignment/common/assignment.ts @@ -0,0 +1,116 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IExperimentationFilterProvider } from 'tas-client-umd'; + +export const ASSIGNMENT_STORAGE_KEY = 'VSCode.ABExp.FeatureData'; +export const ASSIGNMENT_REFETCH_INTERVAL = 0; // no polling + +export interface IAssignmentService { + readonly _serviceBrand: undefined; + getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined>; +} + +export enum TargetPopulation { + Team = 'team', + Internal = 'internal', + Insiders = 'insider', + Public = 'public', +} + +/* +Based upon the official VSCode currently existing filters in the +ExP backend for the VSCode cluster. +https://experimentation.visualstudio.com/Analysis%20and%20Experimentation/_git/AnE.ExP.TAS.TachyonHost.Configuration?path=%2FConfigurations%2Fvscode%2Fvscode.json&version=GBmaster +"X-MSEdge-Market": "detection.market", +"X-FD-Corpnet": "detection.corpnet", +"X-VSCode–AppVersion": "appversion", +"X-VSCode-Build": "build", +"X-MSEdge-ClientId": "clientid", +"X-VSCode-ExtensionName": "extensionname", +"X-VSCode-TargetPopulation": "targetpopulation", +"X-VSCode-Language": "language" +*/ +export enum Filters { + /** + * The market in which the extension is distributed. + */ + Market = 'X-MSEdge-Market', + + /** + * The corporation network. + */ + CorpNet = 'X-FD-Corpnet', + + /** + * Version of the application which uses experimentation service. + */ + ApplicationVersion = 'X-VSCode-AppVersion', + + /** + * Insiders vs Stable. + */ + Build = 'X-VSCode-Build', + + /** + * Client Id which is used as primary unit for the experimentation. + */ + ClientId = 'X-MSEdge-ClientId', + + /** + * Extension header. + */ + ExtensionName = 'X-VSCode-ExtensionName', + + /** + * The language in use by VS Code + */ + Language = 'X-VSCode-Language', + + /** + * The target population. + * This is used to separate internal, early preview, GA, etc. + */ + TargetPopulation = 'X-VSCode-TargetPopulation', +} + +export class AssignmentFilterProvider implements IExperimentationFilterProvider { + constructor( + private version: string, + private appName: string, + private machineId: string, + private targetPopulation: TargetPopulation + ) { } + + getFilterValue(filter: string): string | null { + switch (filter) { + case Filters.ApplicationVersion: + return this.version; // productService.version + case Filters.Build: + return this.appName; // productService.nameLong + case Filters.ClientId: + return this.machineId; + case Filters.Language: + return platform.language; + case Filters.ExtensionName: + return 'vscode-core'; // always return vscode-core for exp service + case Filters.TargetPopulation: + return this.targetPopulation; + default: + return ''; + } + } + + getFilters(): Map<string, any> { + let filters: Map<string, any> = new Map<string, any>(); + let filterValues = Object.values(Filters); + for (let value of filterValues) { + filters.set(value, this.getFilterValue(value)); + } + + return filters; + } +} diff --git a/src/vs/platform/assignment/common/assignmentService.ts b/src/vs/platform/assignment/common/assignmentService.ts new file mode 100644 index 00000000000..ad2997b1fec --- /dev/null +++ b/src/vs/platform/assignment/common/assignmentService.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 type { IExperimentationTelemetry, ExperimentationService as TASClient, IKeyValueStorage } from 'tas-client-umd'; +import { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { getTelemetryLevel } from 'vs/platform/telemetry/common/telemetryUtils'; +import { AssignmentFilterProvider, ASSIGNMENT_REFETCH_INTERVAL, ASSIGNMENT_STORAGE_KEY, IAssignmentService, TargetPopulation } from 'vs/platform/assignment/common/assignment'; + +class NullAssignmentServiceTelemetry implements IExperimentationTelemetry { + constructor() { } + + setSharedProperty(name: string, value: string): void { + // noop due to lack of telemetry service + } + + postEvent(eventName: string, props: Map<string, string>): void { + // noop due to lack of telemetry service + } +} + +export abstract class BaseAssignmentService implements IAssignmentService { + _serviceBrand: undefined; + protected tasClient: Promise<TASClient> | undefined; + private networkInitialized = false; + private overrideInitDelay: Promise<void>; + + protected get experimentsEnabled(): boolean { + return this.configurationService.getValue('workbench.enableExperiments') === true; + } + + constructor( + private readonly getMachineId: () => Promise<string>, + private readonly configurationService: IConfigurationService, + protected readonly productService: IProductService, + protected telemetry: IExperimentationTelemetry, + private keyValueStorage?: IKeyValueStorage + ) { + + if (productService.tasConfig && this.experimentsEnabled && getTelemetryLevel(this.configurationService) === TelemetryLevel.USAGE) { + this.tasClient = this.setupTASClient(); + } + + // For development purposes, configure the delay until tas local tas treatment ovverrides are available + const overrideDelaySetting = this.configurationService.getValue('experiments.overrideDelay'); + const overrideDelay = typeof overrideDelaySetting === 'number' ? overrideDelaySetting : 0; + this.overrideInitDelay = new Promise(resolve => setTimeout(resolve, overrideDelay)); + } + + async getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined> { + // For development purposes, allow overriding tas assignments to test variants locally. + await this.overrideInitDelay; + const override = this.configurationService.getValue<T>('experiments.override.' + name); + if (override !== undefined) { + return override; + } + + if (!this.tasClient) { + return undefined; + } + + if (!this.experimentsEnabled) { + return undefined; + } + + let result: T | undefined; + const client = await this.tasClient; + + // The TAS client is initialized but we need to check if the initial fetch has completed yet + // If it is complete, return a cached value for the treatment + // If not, use the async call with `checkCache: true`. This will allow the module to return a cached value if it is present. + // Otherwise it will await the initial fetch to return the most up to date value. + if (this.networkInitialized) { + result = client.getTreatmentVariable<T>('vscode', name); + } else { + result = await client.getTreatmentVariableAsync<T>('vscode', name, true); + } + + result = client.getTreatmentVariable<T>('vscode', name); + return result; + } + + private async setupTASClient(): Promise<TASClient> { + const targetPopulation = this.productService.quality === 'stable' ? TargetPopulation.Public : TargetPopulation.Insiders; + const machineId = await this.getMachineId(); + const filterProvider = new AssignmentFilterProvider( + this.productService.version, + this.productService.nameLong, + machineId, + targetPopulation + ); + + const tasConfig = this.productService.tasConfig!; + const tasClient = new (await import('tas-client-umd')).ExperimentationService({ + filterProviders: [filterProvider], + telemetry: this.telemetry, + storageKey: ASSIGNMENT_STORAGE_KEY, + keyValueStorage: this.keyValueStorage, + featuresTelemetryPropertyName: tasConfig.featuresTelemetryPropertyName, + assignmentContextTelemetryPropertyName: tasConfig.assignmentContextTelemetryPropertyName, + telemetryEventName: tasConfig.telemetryEventName, + endpoint: tasConfig.endpoint, + refetchInterval: ASSIGNMENT_REFETCH_INTERVAL, + }); + + await tasClient.initializePromise; + tasClient.initialFetch.then(() => this.networkInitialized = true); + + return tasClient; + } +} + +export class AssignmentService extends BaseAssignmentService { + constructor( + machineId: string, + configurationService: IConfigurationService, + productService: IProductService) { + super(() => Promise.resolve(machineId), configurationService, productService, new NullAssignmentServiceTelemetry()); + } +} diff --git a/src/vs/platform/configuration/test/common/testConfigurationService.ts b/src/vs/platform/configuration/test/common/testConfigurationService.ts index 116b0d45f96..9eac534a914 100644 --- a/src/vs/platform/configuration/test/common/testConfigurationService.ts +++ b/src/vs/platform/configuration/test/common/testConfigurationService.ts @@ -6,13 +6,14 @@ import { Emitter } from 'vs/base/common/event'; import { TernarySearchTree } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; -import { getConfigurationKeys, getConfigurationValue, IConfigurationOverrides, IConfigurationService, IConfigurationValue, isConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; +import { getConfigurationKeys, getConfigurationValue, IConfigurationChangeEvent, IConfigurationOverrides, IConfigurationService, IConfigurationValue, isConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; export class TestConfigurationService implements IConfigurationService { public _serviceBrand: undefined; private configuration: any; - readonly onDidChangeConfiguration = new Emitter<any>().event; + readonly onDidChangeConfigurationEmitter = new Emitter<IConfigurationChangeEvent>(); + readonly onDidChangeConfiguration = this.onDidChangeConfigurationEmitter.event; constructor(configuration?: any) { this.configuration = configuration || Object.create(null); diff --git a/src/vs/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index 661f4eafdff..b7eca730c6e 100644 --- a/src/vs/platform/contextkey/common/contextkey.ts +++ b/src/vs/platform/contextkey/common/contextkey.ts @@ -81,50 +81,45 @@ export abstract class ContextKeyExpr { public static false(): ContextKeyExpression { return ContextKeyFalseExpr.INSTANCE; } - public static true(): ContextKeyExpression { return ContextKeyTrueExpr.INSTANCE; } - public static has(key: string): ContextKeyExpression { return ContextKeyDefinedExpr.create(key); } - public static equals(key: string, value: any): ContextKeyExpression { return ContextKeyEqualsExpr.create(key, value); } - public static notEquals(key: string, value: any): ContextKeyExpression { return ContextKeyNotEqualsExpr.create(key, value); } - public static regex(key: string, value: RegExp): ContextKeyExpression { return ContextKeyRegexExpr.create(key, value); } - public static in(key: string, value: string): ContextKeyExpression { return ContextKeyInExpr.create(key, value); } - public static not(key: string): ContextKeyExpression { return ContextKeyNotExpr.create(key); } - public static and(...expr: Array<ContextKeyExpression | undefined | null>): ContextKeyExpression | undefined { return ContextKeyAndExpr.create(expr, null); } - public static or(...expr: Array<ContextKeyExpression | undefined | null>): ContextKeyExpression | undefined { return ContextKeyOrExpr.create(expr, null, true); } - - public static greater(key: string, value: any): ContextKeyExpression { + public static greater(key: string, value: number): ContextKeyExpression { return ContextKeyGreaterExpr.create(key, value); } - - public static less(key: string, value: any): ContextKeyExpression { + public static greaterEquals(key: string, value: number): ContextKeyExpression { + return ContextKeyGreaterEqualsExpr.create(key, value); + } + public static smaller(key: string, value: number): ContextKeyExpression { return ContextKeySmallerExpr.create(key, value); } + public static smallerEquals(key: string, value: number): ContextKeyExpression { + return ContextKeySmallerEqualsExpr.create(key, value); + } public static deserialize(serialized: string | null | undefined, strict: boolean = false): ContextKeyExpression | undefined { if (!serialized) { @@ -741,17 +736,30 @@ export class ContextKeyNotExpr implements IContextKeyExpression { } } +function withFloatOrStr<T extends ContextKeyExpression>(value: any, callback: (value: number | string) => T): T | ContextKeyFalseExpr { + if (typeof value === 'string') { + const n = parseFloat(value); + if (!isNaN(n)) { + value = n; + } + } + if (typeof value === 'string' || typeof value === 'number') { + return callback(value); + } + return ContextKeyFalseExpr.INSTANCE; +} + export class ContextKeyGreaterExpr implements IContextKeyExpression { - public static create(key: string, value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression { - return new ContextKeyGreaterExpr(key, value, negated); + public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression { + return withFloatOrStr(_value, (value) => new ContextKeyGreaterExpr(key, value, negated)); } public readonly type = ContextKeyExprType.Greater; private constructor( private readonly key: string, - private readonly value: any, + private readonly value: number | string, private negated: ContextKeyExpression | null ) { } @@ -774,7 +782,10 @@ export class ContextKeyGreaterExpr implements IContextKeyExpression { } public evaluate(context: IContext): boolean { - return (parseFloat(<any>context.getValue(this.key)) > parseFloat(this.value)); + if (typeof this.value === 'string') { + return false; + } + return (parseFloat(<any>context.getValue(this.key)) > this.value); } public serialize(): string { @@ -799,15 +810,15 @@ export class ContextKeyGreaterExpr implements IContextKeyExpression { export class ContextKeyGreaterEqualsExpr implements IContextKeyExpression { - public static create(key: string, value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression { - return new ContextKeyGreaterEqualsExpr(key, value, negated); + public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression { + return withFloatOrStr(_value, (value) => new ContextKeyGreaterEqualsExpr(key, value, negated)); } public readonly type = ContextKeyExprType.GreaterEquals; private constructor( private readonly key: string, - private readonly value: any, + private readonly value: number | string, private negated: ContextKeyExpression | null ) { } @@ -830,7 +841,10 @@ export class ContextKeyGreaterEqualsExpr implements IContextKeyExpression { } public evaluate(context: IContext): boolean { - return (parseFloat(<any>context.getValue(this.key)) >= parseFloat(this.value)); + if (typeof this.value === 'string') { + return false; + } + return (parseFloat(<any>context.getValue(this.key)) >= this.value); } public serialize(): string { @@ -855,15 +869,15 @@ export class ContextKeyGreaterEqualsExpr implements IContextKeyExpression { export class ContextKeySmallerExpr implements IContextKeyExpression { - public static create(key: string, value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression { - return new ContextKeySmallerExpr(key, value, negated); + public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression { + return withFloatOrStr(_value, (value) => new ContextKeySmallerExpr(key, value, negated)); } public readonly type = ContextKeyExprType.Smaller; private constructor( private readonly key: string, - private readonly value: any, + private readonly value: number | string, private negated: ContextKeyExpression | null ) { } @@ -887,7 +901,10 @@ export class ContextKeySmallerExpr implements IContextKeyExpression { } public evaluate(context: IContext): boolean { - return (parseFloat(<any>context.getValue(this.key)) < parseFloat(this.value)); + if (typeof this.value === 'string') { + return false; + } + return (parseFloat(<any>context.getValue(this.key)) < this.value); } public serialize(): string { @@ -912,15 +929,15 @@ export class ContextKeySmallerExpr implements IContextKeyExpression { export class ContextKeySmallerEqualsExpr implements IContextKeyExpression { - public static create(key: string, value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression { - return new ContextKeySmallerEqualsExpr(key, value, negated); + public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression { + return withFloatOrStr(_value, (value) => new ContextKeySmallerEqualsExpr(key, value, negated)); } public readonly type = ContextKeyExprType.SmallerEquals; private constructor( private readonly key: string, - private readonly value: any, + private readonly value: number | string, private negated: ContextKeyExpression | null ) { } @@ -944,7 +961,10 @@ export class ContextKeySmallerEqualsExpr implements IContextKeyExpression { } public evaluate(context: IContext): boolean { - return (parseFloat(<any>context.getValue(this.key)) <= parseFloat(this.value)); + if (typeof this.value === 'string') { + return false; + } + return (parseFloat(<any>context.getValue(this.key)) <= this.value); } public serialize(): string { diff --git a/src/vs/platform/contextkey/test/common/contextkey.test.ts b/src/vs/platform/contextkey/test/common/contextkey.test.ts index 165ff2ead59..94f807c0e38 100644 --- a/src/vs/platform/contextkey/test/common/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/common/contextkey.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; -import { ContextKeyExpr, implies } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, ContextKeyExpression, implies } from 'vs/platform/contextkey/common/contextkey'; function createContext(ctx: any) { return { @@ -45,6 +45,19 @@ suite('ContextKeyExpr', () => { assert(a.equals(b), 'expressions should be equal'); }); + test('issue #134942: Equals in comparator expressions', () => { + function testEquals(expr: ContextKeyExpression | undefined, str: string): void { + const deserialized = ContextKeyExpr.deserialize(str); + assert.ok(expr); + assert.ok(deserialized); + assert.strictEqual(expr.equals(deserialized), true, str); + } + testEquals(ContextKeyExpr.greater('value', 0), 'value > 0'); + testEquals(ContextKeyExpr.greaterEquals('value', 0), 'value >= 0'); + testEquals(ContextKeyExpr.smaller('value', 0), 'value < 0'); + testEquals(ContextKeyExpr.smallerEquals('value', 0), 'value <= 0'); + }); + test('normalize', () => { let key1IsTrue = ContextKeyExpr.equals('key1', true); let key1IsNotFalse = ContextKeyExpr.notEquals('key1', false); diff --git a/src/vs/platform/diagnostics/node/diagnosticsService.ts b/src/vs/platform/diagnostics/node/diagnosticsService.ts index 81b53140b5e..72971fa7795 100644 --- a/src/vs/platform/diagnostics/node/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/node/diagnosticsService.ts @@ -56,7 +56,9 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P { tag: 'sln', filePattern: /^.+\.sln$/i }, { tag: 'csproj', filePattern: /^.+\.csproj$/i }, { tag: 'cmake', filePattern: /^.+\.cmake$/i }, - { tag: 'github-actions', filePattern: /^.+\.yml$/i, relativePathPattern: /^\.github(?:\/|\\)workflows$/i } + { tag: 'github-actions', filePattern: /^.+\.yml$/i, relativePathPattern: /^\.github(?:\/|\\)workflows$/i }, + { tag: 'devcontainer.json', filePattern: /^devcontainer\.json$/i }, + { tag: 'dockerfile', filePattern: /^(dockerfile|docker\-compose\.ya?ml)$/i } ]; const fileTypes = new Map<string, number>(); diff --git a/src/vs/platform/environment/node/shellEnv.ts b/src/vs/platform/environment/node/shellEnv.ts index 5d3fc1b38bd..de217e36abe 100644 --- a/src/vs/platform/environment/node/shellEnv.ts +++ b/src/vs/platform/environment/node/shellEnv.ts @@ -134,7 +134,12 @@ async function doResolveUnixShellEnv(logService: ILogService, token: Cancellatio shellArgs = ['-Login', '-Command']; } else { command = `'${process.execPath}' -p '"${mark}" + JSON.stringify(process.env) + "${mark}"'`; - shellArgs = ['-ilc']; + + if (name === 'tcsh') { + shellArgs = ['-ic']; + } else { + shellArgs = ['-ilc']; + } } logService.trace('getUnixShellEnvironment#spawn', JSON.stringify(shellArgs), command); diff --git a/src/vs/platform/environment/test/node/nativeModules.test.ts b/src/vs/platform/environment/test/node/nativeModules.test.ts index 3cb1ee1a509..130d0561679 100644 --- a/src/vs/platform/environment/test/node/nativeModules.test.ts +++ b/src/vs/platform/environment/test/node/nativeModules.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { isMacintosh, isWindows } from 'vs/base/common/platform'; +import { isWindows } from 'vs/base/common/platform'; function testErrorMessage(module: string): string { return `Unable to load "${module}" dependency. It was probably not compiled for the right operating system architecture or had missing build tools.`; @@ -53,17 +53,6 @@ suite('Native Modules (all platforms)', () => { }); }); -(!isMacintosh ? suite.skip : suite)('Native Modules (macOS)', () => { - - test('chokidar (fsevents)', async () => { - const chokidar = await import('chokidar'); - const watcher = chokidar.watch(__dirname); - assert.ok(watcher.options.useFsEvents, testErrorMessage('chokidar (fsevents)')); - - return watcher.close(); - }); -}); - (!isWindows ? suite.skip : suite)('Native Modules (Windows)', () => { test('windows-mutex', async () => { diff --git a/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts b/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts index 9bda0d7d610..55aff88346b 100644 --- a/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts +++ b/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts @@ -12,11 +12,8 @@ import { joinPath } from 'vs/base/common/resources'; import { isString } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { createFileSystemProviderError, FileChangeType, FileDeleteOptions, FileOverwriteOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, FileWriteOptions, IFileChange, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions } from 'vs/platform/files/common/files'; - -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'; +import { createFileSystemProviderError, FileChangeType, FileDeleteOptions, FileOverwriteOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, FileWriteOptions, IFileChange, IStat, IWatchOptions } from 'vs/platform/files/common/files'; +import { IndexedDB } from 'vs/base/browser/indexedDB'; // 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); @@ -27,98 +24,6 @@ const ERR_DIR_NOT_EMPTY = createFileSystemProviderError(localize('dirIsNotEmpty' // Arbitrary Internal Errors (should never be thrown in production) const ERR_UNKNOWN_INTERNAL = (message: string) => createFileSystemProviderError(localize('internal', "Internal error occurred in IndexedDB File System Provider. ({0})", message), FileSystemProviderErrorCode.Unknown); -class MissingStoresError extends Error { - constructor(readonly db: IDBDatabase) { - super('Missing stores'); - } -} - -export class IndexedDB { - - private indexedDBPromise: Promise<IDBDatabase | null>; - - constructor() { - this.indexedDBPromise = this.openIndexedDB(INDEXEDDB_VSCODE_DB, 2, [INDEXEDDB_USERDATA_OBJECT_STORE, INDEXEDDB_LOGS_OBJECT_STORE]); - } - - async createFileSystemProvider(scheme: string, store: string, watchCrossWindowChanges: boolean): Promise<IIndexedDBFileSystemProvider | null> { - let fsp: IIndexedDBFileSystemProvider | null = null; - const indexedDB = await this.indexedDBPromise; - if (indexedDB) { - if (indexedDB.objectStoreNames.contains(store)) { - fsp = new IndexedDBFileSystemProvider(scheme, indexedDB, store, watchCrossWindowChanges); - } else { - console.error(`Error while creating indexedDB filesystem provider. Could not find ${store} object store`); - } - } - return fsp; - } - - private async openIndexedDB(name: string, version: number, stores: string[]): Promise<IDBDatabase> { - try { - return await this.createIndexedDB(name, version, stores); - } catch (err) { - if (err instanceof MissingStoresError) { - console.info(`Attempting to recreate the indexedDB once.`, name); - - try { - // Try to delete the db - await this.deleteIndexedDB(err.db); - } catch (error) { - console.error(`Error while deleting the indexedDB`, getErrorMessage(error)); - throw error; - } - - return await this.createIndexedDB(name, version, stores); - } - - throw err; - } - } - - private createIndexedDB(name: string, version: number, stores: string[]): Promise<IDBDatabase> { - return new Promise((c, e) => { - const request = window.indexedDB.open(name, version); - request.onerror = () => e(request.error); - request.onsuccess = () => { - const db = request.result; - for (const store of stores) { - if (!db.objectStoreNames.contains(store)) { - console.error(`Error while opening indexedDB. Could not find ${store} object store`); - e(new MissingStoresError(db)); - return; - } - } - c(db); - }; - request.onupgradeneeded = () => { - const db = request.result; - for (const store of stores) { - if (!db.objectStoreNames.contains(store)) { - db.createObjectStore(store); - } - } - }; - }); - } - - private deleteIndexedDB(indexedDB: IDBDatabase): Promise<void> { - return new Promise((c, e) => { - // Close any opened connections - indexedDB.close(); - - // Delete the db - const deleteRequest = window.indexedDB.deleteDatabase(indexedDB.name); - deleteRequest.onerror = (err) => e(deleteRequest.error); - deleteRequest.onsuccess = () => c(); - }); - } -} - -export interface IIndexedDBFileSystemProvider extends Disposable, IFileSystemProviderWithFileReadWriteCapability { - reset(): Promise<void>; -} - type DirEntry = [string, FileType]; type IndexedDBFileSystemEntry = @@ -319,7 +224,7 @@ class IndexedDBChangesBroadcastChannel extends Disposable { } -class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSystemProvider { +export class IndexedDBFileSystemProvider extends Disposable { readonly capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.FileReadWrite @@ -335,7 +240,7 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy private cachedFiletree: Promise<IndexedDBFileSystemNode> | undefined; private writeManyThrottler: Throttler; - constructor(scheme: string, private readonly database: IDBDatabase, private readonly store: string, watchCrossWindowChanges: boolean) { + constructor(scheme: string, private indexedDB: IndexedDB, private readonly store: string, watchCrossWindowChanges: boolean) { super(); this.writeManyThrottler = new Throttler(); @@ -400,29 +305,19 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy } async readFile(resource: URI): Promise<Uint8Array> { - const buffer = await new Promise<Uint8Array>((c, e) => { - const transaction = this.database.transaction([this.store]); - transaction.oncomplete = () => { - 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`)); - } - } - }; - transaction.onerror = () => e(transaction.error); + const result = await this.indexedDB.runInTransaction(this.store, 'readonly', objectStore => objectStore.get(resource.path)); + if (result === undefined) { + throw ERR_FILE_NOT_FOUND; + } + const buffer = result instanceof Uint8Array ? result : isString(result) ? VSBuffer.fromString(result).buffer : undefined; + if (buffer === undefined) { + throw ERR_UNKNOWN_INTERNAL(`IndexedDB entry at "${resource.path}" in unexpected format`); + } - const objectStore = transaction.objectStore(this.store); - const request = objectStore.get(resource.path); - }); + // update cache + const fileTree = await this.getFiletree(); + fileTree.add(resource.path, { type: 'file', size: buffer.byteLength }); - (await this.getFiletree()).add(resource.path, { type: 'file', size: buffer.byteLength }); return buffer; } @@ -502,70 +397,37 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy private getFiletree(): Promise<IndexedDBFileSystemNode> { if (!this.cachedFiletree) { - this.cachedFiletree = new Promise((c, e) => { - const transaction = this.database.transaction([this.store]); - transaction.oncomplete = () => { - 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); - }; - transaction.onerror = () => e(transaction.error); - - const objectStore = transaction.objectStore(this.store); - const request = objectStore.getAllKeys(); - }); + this.cachedFiletree = (async () => { + const rootNode = new IndexedDBFileSystemNode({ + children: new Map(), + path: '', + type: FileType.Directory + }); + const result = await this.indexedDB.runInTransaction(this.store, 'readonly', objectStore => objectStore.getAllKeys()); + const keys = result.map(key => key.toString()); + keys.forEach(key => rootNode.add(key, { type: 'file' })); + return rootNode; + })(); } return this.cachedFiletree; } private fileWriteBatch: { resource: URI, content: Uint8Array }[] = []; private async writeMany() { - return new Promise<void>((c, e) => { - const fileBatch = this.fileWriteBatch; - this.fileWriteBatch = []; - if (fileBatch.length === 0) { - return c(); - } - - const transaction = this.database.transaction([this.store], 'readwrite'); - transaction.oncomplete = () => c(); - transaction.onerror = () => e(transaction.error); - const objectStore = transaction.objectStore(this.store); - for (const entry of fileBatch) { - objectStore.put(entry.content, entry.resource.path); - } - }); + if (this.fileWriteBatch.length) { + const fileBatch = this.fileWriteBatch.splice(0, this.fileWriteBatch.length); + await this.indexedDB.runInTransaction(this.store, 'readwrite', objectStore => fileBatch.map(entry => objectStore.put(entry.content, entry.resource.path))); + } } - private deleteKeys(keys: string[]): Promise<void> { - return new Promise((c, e) => { - if (keys.length === 0) { - return c(); - } - - const transaction = this.database.transaction([this.store], 'readwrite'); - transaction.oncomplete = () => c(); - transaction.onerror = () => e(transaction.error); - const objectStore = transaction.objectStore(this.store); - for (const key of keys) { - objectStore.delete(key); - } - }); + private async deleteKeys(keys: string[]): Promise<void> { + if (keys.length) { + await this.indexedDB.runInTransaction(this.store, 'readwrite', objectStore => keys.map(key => objectStore.delete(key))); + } } - reset(): Promise<void> { - return new Promise((c, e) => { - const transaction = this.database.transaction([this.store], 'readwrite'); - transaction.oncomplete = () => c(); - transaction.onerror = () => e(transaction.error); - - const objectStore = transaction.objectStore(this.store); - objectStore.clear(); - }); + async reset(): Promise<void> { + await this.indexedDB.runInTransaction(this.store, 'readwrite', objectStore => objectStore.clear()); } + } diff --git a/src/vs/platform/files/common/ipcFileSystemProvider.ts b/src/vs/platform/files/common/ipcFileSystemProvider.ts index b01ac4ff8f2..c85d2436745 100644 --- a/src/vs/platform/files/common/ipcFileSystemProvider.ts +++ b/src/vs/platform/files/common/ipcFileSystemProvider.ts @@ -7,7 +7,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { canceled } from 'vs/base/common/errors'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { newWriteableStream, ReadableStreamEventPayload, ReadableStreamEvents } from 'vs/base/common/stream'; import { URI, UriComponents } from 'vs/base/common/uri'; @@ -19,13 +19,13 @@ import { createFileSystemProviderError, FileChangeType, FileDeleteOptions, FileO * An implementation of a file system provider that is backed by a `IChannel` * and thus implemented via IPC on a different process. */ -export abstract class IPCFileSystemProvider extends Disposable implements +export class IPCFileSystemProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability { - constructor(private readonly channel: IChannel) { + constructor(private readonly channel: IChannel, private readonly extraCapabilities: { trash?: boolean, pathCaseSensitive?: boolean }) { super(); this.registerFileChangeListeners(); @@ -33,24 +33,28 @@ export abstract class IPCFileSystemProvider extends Disposable implements //#region File Capabilities - private readonly _onDidChangeCapabilities = this._register(new Emitter<void>()); - readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event; + readonly onDidChangeCapabilities: Event<void> = Event.None; - private _capabilities = FileSystemProviderCapabilities.FileReadWrite - | FileSystemProviderCapabilities.FileOpenReadWriteClose - | FileSystemProviderCapabilities.FileReadStream - | FileSystemProviderCapabilities.FileFolderCopy - | FileSystemProviderCapabilities.FileWriteUnlock; - get capabilities(): FileSystemProviderCapabilities { return this._capabilities; } + private _capabilities: FileSystemProviderCapabilities | undefined; + get capabilities(): FileSystemProviderCapabilities { + if (!this._capabilities) { + this._capabilities = + FileSystemProviderCapabilities.FileReadWrite | + FileSystemProviderCapabilities.FileOpenReadWriteClose | + FileSystemProviderCapabilities.FileReadStream | + FileSystemProviderCapabilities.FileFolderCopy | + FileSystemProviderCapabilities.FileWriteUnlock; - protected setCaseSensitive(isCaseSensitive: boolean) { - if (isCaseSensitive) { - this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive; - } else { - this._capabilities &= ~FileSystemProviderCapabilities.PathCaseSensitive; + if (this.extraCapabilities.pathCaseSensitive) { + this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive; + } + + if (this.extraCapabilities.trash) { + this._capabilities |= FileSystemProviderCapabilities.Trash; + } } - this._onDidChangeCapabilities.fire(); + return this._capabilities; } //#endregion diff --git a/src/vs/platform/files/common/watcher.ts b/src/vs/platform/files/common/watcher.ts index dd4e8298cee..4314712e66b 100644 --- a/src/vs/platform/files/common/watcher.ts +++ b/src/vs/platform/files/common/watcher.ts @@ -162,8 +162,8 @@ export interface IWatchRequest { excludes: string[]; /** - * @deprecated TODO@bpasero TODO@aeschli remove me once WSL1 - * support ends. + * @deprecated this only exists for WSL1 support and should never + * be used in any other case. */ pollingInterval?: number; } diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index 1c1a1284972..cb867f1a057 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -23,10 +23,8 @@ import { readFileIntoStream } from 'vs/platform/files/common/io'; import { FileWatcher as NodeJSWatcherService } from 'vs/platform/files/node/watcher/nodejs/watcherService'; import { FileWatcher as NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/watcherService'; import { FileWatcher as ParcelWatcherService } from 'vs/platform/files/node/watcher/parcel/watcherService'; -import { FileWatcher as UnixWatcherService } from 'vs/platform/files/node/watcher/unix/watcherService'; import { IDiskFileChange, ILogMessage, IWatchRequest, WatcherService } from 'vs/platform/files/common/watcher'; import { ILogService } from 'vs/platform/log/common/log'; -import product from 'vs/platform/product/common/product'; import { AbstractDiskFileSystemProvider } from 'vs/platform/files/common/diskFileSystemProvider'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -48,8 +46,8 @@ export interface IWatcherOptions { * If `true`, will enable polling for all watchers, otherwise * will enable it for paths included in the string array. * - * @deprecated TODO@bpasero TODO@aeschli remove me once WSL1 - * support ends. + * @deprecated this only exists for WSL1 support and should never + * be used in any other case. */ usePolling: boolean | string[]; @@ -57,8 +55,8 @@ export interface IWatcherOptions { * If polling is enabled (via `usePolling`), defines the duration * in which the watcher will poll for changes. * - * @deprecated TODO@bpasero TODO@aeschli remove me once WSL1 - * support ends. + * @deprecated this only exists for WSL1 support and should never + * be used in any other case. */ pollingInterval?: number; } @@ -570,25 +568,13 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple let enableLegacyWatcher = false; if (this.options?.watcher?.usePolling) { - enableLegacyWatcher = false; // can use Parcel watcher for when polling is required + enableLegacyWatcher = false; // must use Parcel watcher for when polling is required } else { - if (this.options?.legacyWatcher === 'on' || this.options?.legacyWatcher === 'off') { - enableLegacyWatcher = this.options.legacyWatcher === 'on'; // setting always wins - } else { - if (product.quality === 'stable') { - // in stable use legacy for single folder workspaces - // TODO@bpasero remove me eventually - enableLegacyWatcher = folders === 1; - } - } + enableLegacyWatcher = this.options?.legacyWatcher === 'on'; // setting always wins } if (enableLegacyWatcher) { - if (isLinux) { - watcherImpl = UnixWatcherService; - } else { - watcherImpl = NsfwWatcherService; - } + watcherImpl = NsfwWatcherService; } else { watcherImpl = ParcelWatcherService; } diff --git a/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts b/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts deleted file mode 100644 index 495a823a0c6..00000000000 --- a/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts +++ /dev/null @@ -1,374 +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 chokidar from 'chokidar'; -import * as fs from 'fs'; -import * as gracefulFs from 'graceful-fs'; -import { equals } from 'vs/base/common/arrays'; -import { ThrottledDelayer } from 'vs/base/common/async'; -import { Emitter } from 'vs/base/common/event'; -import { isEqualOrParent } from 'vs/base/common/extpath'; -import { match, parse, ParsedPattern } from 'vs/base/common/glob'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { normalizeNFC } from 'vs/base/common/normalization'; -import { isLinux, isMacintosh } from 'vs/base/common/platform'; -import { realcaseSync } from 'vs/base/node/extpath'; -import { FileChangeType } from 'vs/platform/files/common/files'; -import { IWatcherOptions, IWatcherService } from 'vs/platform/files/node/watcher/unix/watcher'; -import { IDiskFileChange, ILogMessage, IWatchRequest, normalizeFileChanges } from 'vs/platform/files/common/watcher'; - -gracefulFs.gracefulify(fs); // enable gracefulFs - -process.noAsar = true; // disable ASAR support in watcher process - -interface IWatcher { - requests: ExtendedWatcherRequest[]; - stop(): Promise<void>; -} - -interface ExtendedWatcherRequest extends IWatchRequest { - parsedPattern?: ParsedPattern; -} - -export class ChokidarWatcherService extends Disposable implements IWatcherService { - - private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms) - private static readonly EVENT_SPAM_WARNING_THRESHOLD = 60 * 1000; // warn after certain time span of event spam - - private readonly _onDidChangeFile = this._register(new Emitter<IDiskFileChange[]>()); - readonly onDidChangeFile = this._onDidChangeFile.event; - - private readonly _onDidLogMessage = this._register(new Emitter<ILogMessage>()); - readonly onDidLogMessage = this._onDidLogMessage.event; - - private watchers = new Map<string, IWatcher>(); - - private _watcherCount = 0; - get wacherCount() { return this._watcherCount; } - - private pollingInterval?: number; - private usePolling?: boolean | string[]; - private verboseLogging: boolean | undefined; - - private spamCheckStartTime: number | undefined; - private spamWarningLogged: boolean | undefined; - private enospcErrorLogged: boolean | undefined; - - async init(options: IWatcherOptions): Promise<void> { - this.pollingInterval = options.pollingInterval; - this.usePolling = options.usePolling; - this.watchers.clear(); - this._watcherCount = 0; - this.verboseLogging = options.verboseLogging; - } - - async setVerboseLogging(enabled: boolean): Promise<void> { - this.verboseLogging = enabled; - } - - async watch(requests: IWatchRequest[]): Promise<void> { - const watchers = new Map<string, IWatcher>(); - const newRequests: string[] = []; - - const requestsByBasePath = normalizeRoots(requests); - - // evaluate new & remaining watchers - for (const basePath in requestsByBasePath) { - const watcher = this.watchers.get(basePath); - if (watcher && isEqualRequests(watcher.requests, requestsByBasePath[basePath])) { - watchers.set(basePath, watcher); - this.watchers.delete(basePath); - } else { - newRequests.push(basePath); - } - } - - // stop all old watchers - for (const [, watcher] of this.watchers) { - await watcher.stop(); - } - - // start all new watchers - for (const basePath of newRequests) { - const requests = requestsByBasePath[basePath]; - watchers.set(basePath, this.doWatch(basePath, requests)); - } - - this.watchers = watchers; - } - - private doWatch(basePath: string, requests: IWatchRequest[]): IWatcher { - const pollingInterval = this.pollingInterval || 5000; - let usePolling = this.usePolling; // boolean or a list of path patterns - if (Array.isArray(usePolling)) { - // switch to polling if one of the paths matches with a watched path - usePolling = usePolling.some(pattern => requests.some(request => match(pattern, request.path))); - } - - const watcherOpts: chokidar.WatchOptions = { - ignoreInitial: true, - ignorePermissionErrors: true, - followSymlinks: true, // this is the default of chokidar and supports file events through symlinks - interval: pollingInterval, // while not used in normal cases, if any error causes chokidar to fallback to polling, increase its intervals - binaryInterval: pollingInterval, - usePolling, - disableGlobbing: true // fix https://github.com/microsoft/vscode/issues/4586 - }; - - const excludes: string[] = []; - - const isSingleFolder = requests.length === 1; - if (isSingleFolder) { - excludes.push(...requests[0].excludes); // if there's only one request, use the built-in ignore-filterering - } - - if ((isMacintosh || isLinux) && (basePath.length === 0 || basePath === '/')) { - excludes.push('/dev/**'); - if (isLinux) { - excludes.push('/proc/**', '/sys/**'); - } - } - - excludes.push('**/*.asar'); // Ensure we never recurse into ASAR archives - - watcherOpts.ignored = excludes; - - // Chokidar fails when the basePath does not match case-identical to the path on disk - // so we have to find the real casing of the path and do some path massaging to fix this - // see https://github.com/paulmillr/chokidar/issues/418 - const realBasePath = isMacintosh ? (realcaseSync(basePath) || basePath) : basePath; - const realBasePathLength = realBasePath.length; - const realBasePathDiffers = (basePath !== realBasePath); - - if (realBasePathDiffers) { - this.warn(`Watcher basePath does not match version on disk and was corrected (original: ${basePath}, real: ${realBasePath})`); - } - - this.debug(`Start watching: ${realBasePath}, excludes: ${excludes.join(',')}, usePolling: ${usePolling ? 'true, interval ' + pollingInterval : 'false'}`); - - let chokidarWatcher: chokidar.FSWatcher | null = chokidar.watch(realBasePath, watcherOpts); - this._watcherCount++; - - // Detect if for some reason the native watcher library fails to load - if (isMacintosh && chokidarWatcher.options && !chokidarWatcher.options.useFsEvents) { - this.warn('Watcher is not using native fsevents library and is falling back to unefficient polling.'); - } - - let undeliveredFileEvents: IDiskFileChange[] = []; - let fileEventDelayer: ThrottledDelayer<undefined> | null = new ThrottledDelayer(ChokidarWatcherService.FS_EVENT_DELAY); - - const watcher: IWatcher = { - requests, - stop: async () => { - try { - if (this.verboseLogging) { - this.log(`Stop watching: ${basePath}]`); - } - - if (chokidarWatcher) { - await chokidarWatcher.close(); - this._watcherCount--; - chokidarWatcher = null; - } - - if (fileEventDelayer) { - fileEventDelayer.cancel(); - fileEventDelayer = null; - } - } catch (error) { - this.warn('Error while stopping watcher: ' + error.toString()); - } - } - }; - - chokidarWatcher.on('all', (type: string, path: string) => { - if (isMacintosh) { - // Mac: uses NFD unicode form on disk, but we want NFC - // See also https://github.com/nodejs/node/issues/2165 - path = normalizeNFC(path); - } - - if (path.indexOf(realBasePath) < 0) { - return; // we really only care about absolute paths here in our basepath context here - } - - // Make sure to convert the path back to its original basePath form if the realpath is different - if (realBasePathDiffers) { - path = basePath + path.substr(realBasePathLength); - } - - let eventType: FileChangeType; - switch (type) { - case 'change': - eventType = FileChangeType.UPDATED; - break; - case 'add': - case 'addDir': - eventType = FileChangeType.ADDED; - break; - case 'unlink': - case 'unlinkDir': - eventType = FileChangeType.DELETED; - break; - default: - return; - } - - // if there's more than one request we need to do - // extra filtering due to potentially overlapping roots - if (!isSingleFolder) { - if (isIgnored(path, watcher.requests)) { - return; - } - } - - const event = { type: eventType, path }; - - // Logging - if (this.verboseLogging) { - this.log(`${eventType === FileChangeType.ADDED ? '[ADDED]' : eventType === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${path}`); - } - - // Check for spam - const now = Date.now(); - if (undeliveredFileEvents.length === 0) { - this.spamWarningLogged = false; - this.spamCheckStartTime = now; - } else if (!this.spamWarningLogged && typeof this.spamCheckStartTime === 'number' && this.spamCheckStartTime + ChokidarWatcherService.EVENT_SPAM_WARNING_THRESHOLD < now) { - this.spamWarningLogged = true; - this.warn(`Watcher is busy catching up with ${undeliveredFileEvents.length} file changes in 60 seconds. Latest changed path is "${event.path}"`); - } - - // Add to buffer - undeliveredFileEvents.push(event); - - if (fileEventDelayer) { - - // Delay and send buffer - fileEventDelayer.trigger(async () => { - const events = undeliveredFileEvents; - undeliveredFileEvents = []; - - // Broadcast to clients normalized - const normalizedEvents = normalizeFileChanges(events); - this._onDidChangeFile.fire(normalizedEvents); - - // Logging - if (this.verboseLogging) { - for (const e of normalizedEvents) { - this.log(` >> normalized ${e.type === FileChangeType.ADDED ? '[ADDED]' : e.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${e.path}`); - } - } - - return undefined; - }); - } - }); - - chokidarWatcher.on('error', (error: NodeJS.ErrnoException) => { - if (error) { - - // Specially handle ENOSPC errors that can happen when - // the watcher consumes so many file descriptors that - // we are running into a limit. We only want to warn - // once in this case to avoid log spam. - // See https://github.com/microsoft/vscode/issues/7950 - if (error.code === 'ENOSPC') { - if (!this.enospcErrorLogged) { - this.enospcErrorLogged = true; - this.stop(); - this.error('Inotify limit reached (ENOSPC)'); - } - } else { - this.warn(error.toString()); - } - } - }); - return watcher; - } - - async stop(): Promise<void> { - for (const [, watcher] of this.watchers) { - await watcher.stop(); - } - - this.watchers.clear(); - } - - private log(message: string) { - this._onDidLogMessage.fire({ type: 'trace', message: `[File Watcher (chokidar)] ` + message }); - } - - private debug(message: string) { - this._onDidLogMessage.fire({ type: 'debug', message: `[File Watcher (chokidar)] ` + message }); - } - - private warn(message: string) { - this._onDidLogMessage.fire({ type: 'warn', message: `[File Watcher (chokidar)] ` + message }); - } - - private error(message: string) { - this._onDidLogMessage.fire({ type: 'error', message: `[File Watcher (chokidar)] ` + message }); - } -} - -function isIgnored(path: string, requests: ExtendedWatcherRequest[]): boolean { - for (const request of requests) { - if (request.path === path) { - return false; - } - - if (isEqualOrParent(path, request.path)) { - if (!request.parsedPattern) { - if (request.excludes && request.excludes.length > 0) { - const pattern = `{${request.excludes.join(',')}}`; - request.parsedPattern = parse(pattern); - } else { - request.parsedPattern = () => false; - } - } - - const relPath = path.substr(request.path.length + 1); - if (!request.parsedPattern(relPath)) { - return false; - } - } - } - - return true; -} - -/** - * Normalizes a set of root paths by grouping by the most parent root path. - * equests with Sub paths are skipped if they have the same ignored set as the parent. - */ -export function normalizeRoots(requests: IWatchRequest[]): { [basePath: string]: IWatchRequest[] } { - requests = requests.sort((r1, r2) => r1.path.localeCompare(r2.path)); - - let prevRequest: IWatchRequest | null = null; - const result: { [basePath: string]: IWatchRequest[] } = Object.create(null); - for (const request of requests) { - const basePath = request.path; - const ignored = (request.excludes || []).sort(); - if (prevRequest && (isEqualOrParent(basePath, prevRequest.path))) { - if (!isEqualIgnore(ignored, prevRequest.excludes)) { - result[prevRequest.path].push({ path: basePath, excludes: ignored }); - } - } else { - prevRequest = { path: basePath, excludes: ignored }; - result[basePath] = [prevRequest]; - } - } - - return result; -} - -function isEqualRequests(r1: readonly IWatchRequest[], r2: readonly IWatchRequest[]) { - return equals(r1, r2, (a, b) => a.path === b.path && isEqualIgnore(a.excludes, b.excludes)); -} - -function isEqualIgnore(i1: readonly string[], i2: readonly string[]) { - return equals(i1, i2); -} 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 deleted file mode 100644 index 73e42892f70..00000000000 --- a/src/vs/platform/files/node/watcher/unix/test/chockidarWatcherService.test.ts +++ /dev/null @@ -1,96 +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 platform from 'vs/base/common/platform'; -import { IWatchRequest } from 'vs/platform/files/common/watcher'; - -suite('Chokidar normalizeRoots', async () => { - - // Load `chokidarWatcherService` within the suite to prevent all tests - // from failing to start if `chokidar` was not properly installed - const { normalizeRoots } = await import('vs/platform/files/node/watcher/unix/chokidarWatcherService'); - - function newRequest(basePath: string, ignored: string[] = []): IWatchRequest { - return { path: basePath, excludes: ignored }; - } - - function assertNormalizedRootPath(inputPaths: string[], expectedPaths: string[]) { - const requests = inputPaths.map(path => newRequest(path)); - const actual = normalizeRoots(requests); - assert.deepStrictEqual(Object.keys(actual).sort(), expectedPaths); - } - - function assertNormalizedRequests(inputRequests: IWatchRequest[], expectedRequests: { [path: string]: IWatchRequest[] }) { - const actual = normalizeRoots(inputRequests); - const actualPath = Object.keys(actual).sort(); - const expectedPaths = Object.keys(expectedRequests).sort(); - 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.deepStrictEqual(a, e); - } - } - - test('should not impacts roots that don\'t overlap', () => { - if (platform.isWindows) { - assertNormalizedRootPath(['C:\\a'], ['C:\\a']); - assertNormalizedRootPath(['C:\\a', 'C:\\b'], ['C:\\a', 'C:\\b']); - assertNormalizedRootPath(['C:\\a', 'C:\\b', 'C:\\c\\d\\e'], ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']); - } else { - assertNormalizedRootPath(['/a'], ['/a']); - assertNormalizedRootPath(['/a', '/b'], ['/a', '/b']); - assertNormalizedRootPath(['/a', '/b', '/c/d/e'], ['/a', '/b', '/c/d/e']); - } - }); - - test('should remove sub-folders of other roots', () => { - if (platform.isWindows) { - assertNormalizedRootPath(['C:\\a', 'C:\\a\\b'], ['C:\\a']); - assertNormalizedRootPath(['C:\\a', 'C:\\b', 'C:\\a\\b'], ['C:\\a', 'C:\\b']); - assertNormalizedRootPath(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b'], ['C:\\a', 'C:\\b']); - assertNormalizedRootPath(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d'], ['C:\\a']); - } else { - assertNormalizedRootPath(['/a', '/a/b'], ['/a']); - assertNormalizedRootPath(['/a', '/b', '/a/b'], ['/a', '/b']); - assertNormalizedRootPath(['/b/a', '/a', '/b', '/a/b'], ['/a', '/b']); - assertNormalizedRootPath(['/a', '/a/b', '/a/c/d'], ['/a']); - assertNormalizedRootPath(['/a/c/d/e', '/a/b/d', '/a/c/d', '/a/c/e/f', '/a/b'], ['/a/b', '/a/c/d', '/a/c/e/f']); - } - }); - - test('should remove duplicates', () => { - if (platform.isWindows) { - assertNormalizedRootPath(['C:\\a', 'C:\\a\\', 'C:\\a'], ['C:\\a']); - } else { - assertNormalizedRootPath(['/a', '/a/', '/a'], ['/a']); - assertNormalizedRootPath(['/a', '/b', '/a/b'], ['/a', '/b']); - assertNormalizedRootPath(['/b/a', '/a', '/b', '/a/b'], ['/a', '/b']); - assertNormalizedRootPath(['/a', '/a/b', '/a/c/d'], ['/a']); - } - }); - - test('nested requests', () => { - let p1, p2, p3; - if (platform.isWindows) { - p1 = 'C:\\a'; - p2 = 'C:\\a\\b'; - p3 = 'C:\\a\\b\\c'; - } else { - p1 = '/a'; - p2 = '/a/b'; - p3 = '/a/b/c'; - } - const r1 = newRequest(p1, ['**/*.ts']); - const r2 = newRequest(p2, ['**/*.js']); - const r3 = newRequest(p3, ['**/*.ts']); - assertNormalizedRequests([r1, r2], { [p1]: [r1, r2] }); - assertNormalizedRequests([r2, r1], { [p1]: [r1, r2] }); - assertNormalizedRequests([r1, r2, r3], { [p1]: [r1, r2, r3] }); - assertNormalizedRequests([r1, r3], { [p1]: [r1] }); - assertNormalizedRequests([r2, r3], { [p2]: [r2, r3] }); - }); -}); diff --git a/src/vs/platform/files/node/watcher/unix/watcher.ts b/src/vs/platform/files/node/watcher/unix/watcher.ts deleted file mode 100644 index d8b67fb998a..00000000000 --- a/src/vs/platform/files/node/watcher/unix/watcher.ts +++ /dev/null @@ -1,26 +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 { Event } from 'vs/base/common/event'; -import { IDiskFileChange, ILogMessage, IWatchRequest } from 'vs/platform/files/common/watcher'; - -export interface IWatcherOptions { - pollingInterval?: number; - usePolling?: boolean | string[]; // boolean or a set of glob patterns matching folders that need polling - verboseLogging?: boolean; -} - -export interface IWatcherService { - - readonly onDidChangeFile: Event<IDiskFileChange[]>; - readonly onDidLogMessage: Event<ILogMessage>; - - init(options: IWatcherOptions): Promise<void>; - - watch(paths: IWatchRequest[]): Promise<void>; - setVerboseLogging(enabled: boolean): Promise<void>; - - stop(): Promise<void>; -} diff --git a/src/vs/platform/files/node/watcher/unix/watcherApp.ts b/src/vs/platform/files/node/watcher/unix/watcherApp.ts deleted file mode 100644 index c56a3bbc40f..00000000000 --- a/src/vs/platform/files/node/watcher/unix/watcherApp.ts +++ /dev/null @@ -1,12 +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 { ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; -import { Server } from 'vs/base/parts/ipc/node/ipc.cp'; -import { ChokidarWatcherService } from 'vs/platform/files/node/watcher/unix/chokidarWatcherService'; - -const server = new Server('watcher'); -const service = new ChokidarWatcherService(); -server.registerChannel('watcher', ProxyChannel.fromService(service)); diff --git a/src/vs/platform/files/node/watcher/unix/watcherService.ts b/src/vs/platform/files/node/watcher/unix/watcherService.ts deleted file mode 100644 index 9ce026a3f52..00000000000 --- a/src/vs/platform/files/node/watcher/unix/watcherService.ts +++ /dev/null @@ -1,98 +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 { FileAccess } from 'vs/base/common/network'; -import { getNextTickChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; -import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; -import { IWatcherOptions, IWatcherService } from 'vs/platform/files/node/watcher/unix/watcher'; -import { IDiskFileChange, ILogMessage, IWatchRequest, WatcherService } from 'vs/platform/files/common/watcher'; - -/** - * @deprecated - */ -export class FileWatcher extends WatcherService { - - private static readonly MAX_RESTARTS = 5; - - private service: IWatcherService | undefined; - - private isDisposed = false; - private restartCounter = 0; - - private requests: IWatchRequest[] | undefined = undefined; - - constructor( - private readonly onDidFilesChange: (changes: IDiskFileChange[]) => void, - private readonly onLogMessage: (msg: ILogMessage) => void, - private verboseLogging: boolean, - private readonly watcherOptions: IWatcherOptions = {} - ) { - super(); - - this.startWatching(); - } - - private startWatching(): void { - const client = this._register(new Client( - FileAccess.asFileUri('bootstrap-fork', require).fsPath, - { - serverName: 'File Watcher (chokidar)', - args: ['--type=watcherServiceChokidar'], - env: { - 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 - } - } - )); - - this._register(client.onDidProcessExit(() => { - // our watcher app should never be completed because it keeps on watching. being in here indicates - // that the watcher process died and we want to restart it here. we only do it a max number of times - if (!this.isDisposed) { - if (this.restartCounter <= FileWatcher.MAX_RESTARTS && this.requests) { - this.error('terminated unexpectedly and is restarted again...'); - this.restartCounter++; - this.startWatching(); - this.service?.watch(this.requests); - } else { - this.error('failed to start after retrying for some time, giving up. Please report this as a bug report!'); - } - } - })); - - // Initialize watcher - this.service = ProxyChannel.toService<IWatcherService>(getNextTickChannel(client.getChannel('watcher'))); - this.service.init({ ...this.watcherOptions, verboseLogging: this.verboseLogging }); - - // Wire in event handlers - this._register(this.service.onDidChangeFile(e => !this.isDisposed && this.onDidFilesChange(e))); - this._register(this.service.onDidLogMessage(e => this.onLogMessage(e))); - } - - async setVerboseLogging(verboseLogging: boolean): Promise<void> { - this.verboseLogging = verboseLogging; - - if (!this.isDisposed) { - await this.service?.setVerboseLogging(verboseLogging); - } - } - - error(message: string) { - this.onLogMessage({ type: 'error', message: `[File Watcher (chokidar)] ${message}` }); - } - - async watch(requests: IWatchRequest[]): Promise<void> { - this.requests = requests; - - await this.service?.watch(requests); - } - - override dispose(): void { - this.isDisposed = true; - - super.dispose(); - } -} diff --git a/src/vs/platform/files/test/browser/indexedDBFileService.test.ts b/src/vs/platform/files/test/browser/indexedDBFileService.test.ts index 5799becc5af..b5a8635edec 100644 --- a/src/vs/platform/files/test/browser/indexedDBFileService.test.ts +++ b/src/vs/platform/files/test/browser/indexedDBFileService.test.ts @@ -4,25 +4,25 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { IndexedDB } from 'vs/base/browser/indexedDB'; import { bufferToReadable, bufferToStream, VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { basename, joinPath } from 'vs/base/common/resources'; -import { assertIsDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { flakySuite } from 'vs/base/test/common/testUtils'; -import { IIndexedDBFileSystemProvider, IndexedDB, INDEXEDDB_LOGS_OBJECT_STORE, INDEXEDDB_USERDATA_OBJECT_STORE } from 'vs/platform/files/browser/indexedDBFileSystemProvider'; +import { IndexedDBFileSystemProvider } from 'vs/platform/files/browser/indexedDBFileSystemProvider'; import { FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileSystemProviderErrorCode, FileType, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; import { NullLogService } from 'vs/platform/log/common/log'; -flakySuite('IndexedDB File Service', function () { +flakySuite('IndexedDBFileSystemProvider', function () { const logSchema = 'logs'; let service: FileService; - let logFileProvider: IIndexedDBFileSystemProvider; - let userdataFileProvider: IIndexedDBFileSystemProvider; + let logFileProvider: IndexedDBFileSystemProvider; + let userdataFileProvider: IndexedDBFileSystemProvider; const testDir = '/'; const logfileURIFromPaths = (paths: string[]) => joinPath(URI.from({ scheme: logSchema, path: testDir }), ...paths); @@ -67,11 +67,13 @@ flakySuite('IndexedDB File Service', function () { service = new FileService(logService); disposables.add(service); - logFileProvider = assertIsDefined(await new IndexedDB().createFileSystemProvider(Schemas.file, INDEXEDDB_LOGS_OBJECT_STORE, false)); + const indexedDB = await IndexedDB.create('vscode-web-db-test', 1, ['vscode-userdata-store', 'vscode-logs-store']); + + logFileProvider = new IndexedDBFileSystemProvider(logSchema, indexedDB, 'vscode-logs-store', false); disposables.add(service.registerProvider(logSchema, logFileProvider)); disposables.add(logFileProvider); - userdataFileProvider = assertIsDefined(await new IndexedDB().createFileSystemProvider(logSchema, INDEXEDDB_USERDATA_OBJECT_STORE, true)); + userdataFileProvider = new IndexedDBFileSystemProvider(Schemas.userData, indexedDB, 'vscode-userdata-store', true); disposables.add(service.registerProvider(Schemas.userData, userdataFileProvider)); disposables.add(userdataFileProvider); }; diff --git a/src/vs/platform/layout/browser/zIndexRegistry.ts b/src/vs/platform/layout/browser/zIndexRegistry.ts index 1b0ffe29afa..c8e4e9cf837 100644 --- a/src/vs/platform/layout/browser/zIndexRegistry.ts +++ b/src/vs/platform/layout/browser/zIndexRegistry.ts @@ -64,7 +64,7 @@ class ZIndexRegistry { this.zIndexMap.forEach((zIndex, name) => { ruleBuilder += `${this.getVarName(name)}: ${zIndex};\n`; }); - createCSSRule('*', ruleBuilder, this.styleSheet); + createCSSRule(':root', ruleBuilder, this.styleSheet); } } diff --git a/src/vs/platform/sharedProcess/common/sharedProcessWorkerService.ts b/src/vs/platform/sharedProcess/common/sharedProcessWorkerService.ts index c64dadbe270..9bbf80c574c 100644 --- a/src/vs/platform/sharedProcess/common/sharedProcessWorkerService.ts +++ b/src/vs/platform/sharedProcess/common/sharedProcessWorkerService.ts @@ -20,6 +20,32 @@ export interface ISharedProcessWorkerProcess { type: string; } +export interface IOnDidTerminateSharedProcessWorkerProcess { + + /** + * More information around how the shared process worker + * process terminated. Will be `undefined` in case the + * worker process was terminated normally via APIs + * and will be defined in case the worker process + * terminated on its own, either unexpectedly or + * because it finished. + */ + reason?: ISharedProcessWorkerProcessExit; +} + +export interface ISharedProcessWorkerProcessExit { + + /** + * The shared process worker process exit code if known. + */ + code?: number; + + /** + * The shared process worker process exit signal if known. + */ + signal?: string; +} + export interface ISharedProcessWorkerConfiguration { /** @@ -77,8 +103,12 @@ export interface ISharedProcessWorkerService { * the same process from one window. The intent of these workers is to be reused per * window and the communication channel allows to dynamically update the processes * after the fact. + * + * @returns a promise that resolves then the worker terminated. Provides more details + * about the termination that can be used to figure out if the termination was unexpected + * or not and whether the worker needs to be restarted. */ - createWorker(configuration: ISharedProcessWorkerConfiguration): Promise<void>; + createWorker(configuration: ISharedProcessWorkerConfiguration): Promise<IOnDidTerminateSharedProcessWorkerProcess>; /** * Terminates the process for the provided configuration if any. diff --git a/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerMain.ts b/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerMain.ts index 5fa8f213f7b..03d803ce0f7 100644 --- a/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerMain.ts +++ b/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerMain.ts @@ -11,8 +11,9 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { deepClone } from 'vs/base/common/objects'; +import { withNullAsUndefined } from 'vs/base/common/types'; import { removeDangerousEnvVariables } from 'vs/base/node/processes'; -import { hash, ISharedProcessWorkerConfiguration } from 'vs/platform/sharedProcess/common/sharedProcessWorkerService'; +import { hash, ISharedProcessWorkerConfiguration, ISharedProcessWorkerProcessExit } from 'vs/platform/sharedProcess/common/sharedProcessWorkerService'; import { SharedProcessWorkerMessages, ISharedProcessToWorkerMessage, ISharedProcessWorkerEnvironment, IWorkerToSharedProcessMessage } from 'vs/platform/sharedProcess/electron-browser/sharedProcessWorker'; /** @@ -75,10 +76,11 @@ class SharedProcessWorkerMain { process.spawn(); // Handle self termination of the child process - const listener = Event.once(process.onDidProcessSelfTerminate)(() => { + const listener = Event.once(process.onDidProcessSelfTerminate)(reason => { send({ id: SharedProcessWorkerMessages.SelfTerminated, - configuration + configuration, + message: JSON.stringify(reason) }); }); @@ -108,7 +110,7 @@ class SharedProcessWorkerMain { class SharedProcessWorkerProcess extends Disposable { - private readonly _onDidProcessSelfTerminate = this._register(new Emitter<void>()); + private readonly _onDidProcessSelfTerminate = this._register(new Emitter<ISharedProcessWorkerProcessExit>()); readonly onDidProcessSelfTerminate = this._onDidProcessSelfTerminate.event; private child: ChildProcess | undefined = undefined; @@ -151,7 +153,10 @@ class SharedProcessWorkerProcess extends Disposable { this.child = undefined; - this._onDidProcessSelfTerminate.fire(); + this._onDidProcessSelfTerminate.fire({ + code: withNullAsUndefined(code), + signal: withNullAsUndefined(signal) + }); })); const onMessageEmitter = this._register(new Emitter<VSBuffer>()); diff --git a/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerService.ts b/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerService.ts index c239065bbd9..4f51ff07e2f 100644 --- a/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerService.ts +++ b/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerService.ts @@ -6,11 +6,11 @@ import { ipcRenderer } from 'electron'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; -import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { FileAccess } from 'vs/base/common/network'; import { generateUuid } from 'vs/base/common/uuid'; import { ILogService } from 'vs/platform/log/common/log'; -import { hash, ISharedProcessWorkerConfiguration, ISharedProcessWorkerService } from 'vs/platform/sharedProcess/common/sharedProcessWorkerService'; +import { hash, IOnDidTerminateSharedProcessWorkerProcess, ISharedProcessWorkerConfiguration, ISharedProcessWorkerProcessExit, ISharedProcessWorkerService } from 'vs/platform/sharedProcess/common/sharedProcessWorkerService'; import { SharedProcessWorkerMessages, ISharedProcessToWorkerMessage, IWorkerToSharedProcessMessage } from 'vs/platform/sharedProcess/electron-browser/sharedProcessWorker'; export class SharedProcessWorkerService implements ISharedProcessWorkerService { @@ -18,23 +18,25 @@ export class SharedProcessWorkerService implements ISharedProcessWorkerService { declare readonly _serviceBrand: undefined; private readonly workers = new Map<string /* process module ID */, Promise<SharedProcessWebWorker>>(); - private readonly processes = new Map<number /* process configuration hash */, IDisposable>(); + + private readonly processeDisposables = new Map<number /* process configuration hash */, (reason?: ISharedProcessWorkerProcessExit) => void>(); + private readonly processResolvers = new Map<number /* process configuration hash */, (process: IOnDidTerminateSharedProcessWorkerProcess) => void>(); constructor( @ILogService private readonly logService: ILogService ) { } - async createWorker(configuration: ISharedProcessWorkerConfiguration): Promise<void> { + async createWorker(configuration: ISharedProcessWorkerConfiguration): Promise<IOnDidTerminateSharedProcessWorkerProcess> { const workerLogId = `window: ${configuration.reply.windowId}, moduleId: ${configuration.process.moduleId}`; this.logService.trace(`SharedProcess: createWorker (${workerLogId})`); // Ensure to dispose any existing process for config const configurationHash = hash(configuration); - if (this.processes.has(configurationHash)) { + if (this.processeDisposables.has(configurationHash)) { this.logService.warn(`SharedProcess: createWorker found an existing worker that will be terminated (${workerLogId})`); - this.disposeWorker(configuration); + this.doDisposeWorker(configuration); } const cts = new CancellationTokenSource(); @@ -43,8 +45,8 @@ export class SharedProcessWorkerService implements ISharedProcessWorkerService { let windowPort: MessagePort | undefined = undefined; let workerPort: MessagePort | undefined = undefined; - // Store as process for later disposal - this.processes.set(configurationHash, toDisposable(() => { + // Store as process for termination support + this.processeDisposables.set(configurationHash, (reason?: ISharedProcessWorkerProcessExit) => { // Signal to token cts.dispose(true); @@ -57,14 +59,27 @@ export class SharedProcessWorkerService implements ISharedProcessWorkerService { workerPort?.close(); // Remove from processes - this.processes.delete(configurationHash); - })); + this.processeDisposables.delete(configurationHash); + + // Release process resolvers if any + const processResolver = this.processResolvers.get(configurationHash); + if (processResolver) { + this.processResolvers.delete(configurationHash); + processResolver({ reason }); + } + }); // Acquire a worker for the configuration worker = await this.getOrCreateWebWorker(configuration); + // Keep a promise that will resolve in the future when the + // underlying process terminates. + const onDidTerminate = new Promise<IOnDidTerminateSharedProcessWorkerProcess>(resolve => { + this.processResolvers.set(configurationHash, resolve); + }); + if (cts.token.isCancellationRequested) { - return; + return onDidTerminate; } // Create a `MessageChannel` with 2 ports: @@ -78,7 +93,7 @@ export class SharedProcessWorkerService implements ISharedProcessWorkerService { await worker.spawn(configuration, workerPort, cts.token); if (cts.token.isCancellationRequested) { - return; + return onDidTerminate; } // We cannot just send the `MessagePort` through our protocol back @@ -86,6 +101,8 @@ export class SharedProcessWorkerService implements ISharedProcessWorkerService { // to send it through the main process back to the window. this.logService.trace(`SharedProcess: createWorker sending message port back to window (${workerLogId})`); ipcRenderer.postMessage('vscode:relaySharedProcessWorkerMessageChannel', configuration, [windowPort]); + + return onDidTerminate; } private getOrCreateWebWorker(configuration: ISharedProcessWorkerConfiguration): Promise<SharedProcessWebWorker> { @@ -103,11 +120,10 @@ export class SharedProcessWorkerService implements ISharedProcessWorkerService { const sharedProcessWorker = new SharedProcessWebWorker(configuration.process.type, this.logService); webWorkerPromise = sharedProcessWorker.init(); - // Make sure to run through our normal - // `disposeWorker` call when the process - // terminates by itself. - sharedProcessWorker.onDidProcessSelfTerminate(configuration => { - this.disposeWorker(configuration); + // Make sure to run through our normal `disposeWorker` call + // when the process terminates by itself. + sharedProcessWorker.onDidProcessSelfTerminate(({ configuration, reason }) => { + this.doDisposeWorker(configuration, reason); }); this.workers.set(configuration.process.moduleId, webWorkerPromise); @@ -117,18 +133,22 @@ export class SharedProcessWorkerService implements ISharedProcessWorkerService { } async disposeWorker(configuration: ISharedProcessWorkerConfiguration): Promise<void> { - const processDisposable = this.processes.get(hash(configuration)); + return this.doDisposeWorker(configuration); + } + + private doDisposeWorker(configuration: ISharedProcessWorkerConfiguration, reason?: ISharedProcessWorkerProcessExit): void { + const processDisposable = this.processeDisposables.get(hash(configuration)); if (processDisposable) { this.logService.trace(`SharedProcess: disposeWorker (window: ${configuration.reply.windowId}, moduleId: ${configuration.process.moduleId})`); - processDisposable.dispose(); + processDisposable(reason); } } } class SharedProcessWebWorker extends Disposable { - private readonly _onDidProcessSelfTerminate = this._register(new Emitter<ISharedProcessWorkerConfiguration>()); + private readonly _onDidProcessSelfTerminate = this._register(new Emitter<{ configuration: ISharedProcessWorkerConfiguration, reason: ISharedProcessWorkerProcessExit }>()); readonly onDidProcessSelfTerminate = this._onDidProcessSelfTerminate.event; private readonly workerReady: Promise<Worker> = this.doInit(); @@ -186,8 +206,8 @@ class SharedProcessWebWorker extends Disposable { // Lifecycle: self termination case SharedProcessWorkerMessages.SelfTerminated: - if (configuration) { - this._onDidProcessSelfTerminate.fire(configuration); + if (configuration && message) { + this._onDidProcessSelfTerminate.fire({ configuration, reason: JSON.parse(message) }); } break; diff --git a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index c196c2d918b..36cd750dc0c 100644 --- a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -92,6 +92,10 @@ export class SharedProcess extends Disposable implements ISharedProcess { const disposables = new DisposableStore(); const disposeWorker = (reason: string) => { + if (!this.isAlive()) { + return; // the shared process is already gone, no need to dispose anything + } + this.logService.trace(`SharedProcess: disposing worker (reason: '${reason}')`, configuration); // Only once! @@ -152,14 +156,13 @@ export class SharedProcess extends Disposable implements ISharedProcess { } private send(channel: string, ...args: any[]): void { - const window = this.window; - if (!window || window.isDestroyed() || window.webContents.isDestroyed()) { + if (!this.isAlive()) { this.logService.warn(`Sending IPC message to channel '${channel}' for shared process window that is destroyed`); return; } try { - window.webContents.send(channel, ...args); + this.window?.webContents.send(channel, ...args); } catch (error) { this.logService.warn(`Error sending IPC message to channel '${channel}' of shared process: ${toErrorMessage(error)}`); } @@ -305,4 +308,13 @@ export class SharedProcess extends Disposable implements ISharedProcess { isVisible(): boolean { return this.window?.isVisible() ?? false; } + + private isAlive(): boolean { + const window = this.window; + if (!window) { + return false; + } + + return !window.isDestroyed() && !window.webContents.isDestroyed(); + } } diff --git a/src/vs/platform/storage/common/storage.ts b/src/vs/platform/storage/common/storage.ts index a6a7466efa4..6c60de2fb76 100644 --- a/src/vs/platform/storage/common/storage.ts +++ b/src/vs/platform/storage/common/storage.ts @@ -6,6 +6,7 @@ import { Promises, RunOnceScheduler, runWhenIdle } from 'vs/base/common/async'; import { Emitter, Event, PauseableEmitter } from 'vs/base/common/event'; import { Disposable, dispose, MutableDisposable } from 'vs/base/common/lifecycle'; +import { mark } from 'vs/base/common/performance'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { InMemoryStorageDatabase, IStorage, Storage } from 'vs/base/parts/storage/common/storage'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -271,8 +272,14 @@ export abstract class AbstractStorageService extends Disposable implements IStor if (!this.initializationPromise) { this.initializationPromise = (async () => { - // Ask subclasses to initialize storage - await this.doInitialize(); + // Init all storage locations + mark('code/willInitStorage'); + try { + // Ask subclasses to initialize storage + await this.doInitialize(); + } finally { + mark('code/didInitStorage'); + } // On some OS we do not get enough time to persist state on shutdown (e.g. when // Windows restarts after applying updates). In other cases, VSCode might crash, diff --git a/src/vs/platform/storage/electron-sandbox/storageService.ts b/src/vs/platform/storage/electron-sandbox/storageService.ts index 6e2ac17172b..95c29dbaab3 100644 --- a/src/vs/platform/storage/electron-sandbox/storageService.ts +++ b/src/vs/platform/storage/electron-sandbox/storageService.ts @@ -5,7 +5,6 @@ import { Promises } from 'vs/base/common/async'; import { MutableDisposable } from 'vs/base/common/lifecycle'; -import { mark } from 'vs/base/common/performance'; import { joinPath } from 'vs/base/common/resources'; import { IStorage, Storage } from 'vs/base/parts/storage/common/storage'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -67,17 +66,11 @@ export class NativeStorageService extends AbstractStorageService { } protected async doInitialize(): Promise<void> { - // Init all storage locations - mark('code/willInitStorage'); - try { - await Promises.settled([ - this.globalStorage.init(), - this.workspaceStorage?.init() ?? Promise.resolve() - ]); - } finally { - mark('code/didInitStorage'); - } + await Promises.settled([ + this.globalStorage.init(), + this.workspaceStorage?.init() ?? Promise.resolve() + ]); } protected getStorage(scope: StorageScope): IStorage | undefined { diff --git a/src/vs/platform/telemetry/node/appInsightsAppender.ts b/src/vs/platform/telemetry/node/appInsightsAppender.ts index 1ce90fd63da..cec826e949f 100644 --- a/src/vs/platform/telemetry/node/appInsightsAppender.ts +++ b/src/vs/platform/telemetry/node/appInsightsAppender.ts @@ -8,7 +8,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { mixin } from 'vs/base/common/objects'; import { ITelemetryAppender, validateTelemetryData } from 'vs/platform/telemetry/common/telemetryUtils'; -async function getClient(aiKey: string): Promise<TelemetryClient> { +async function getClient(aiKey: string, testCollector: boolean): Promise<TelemetryClient> { const appInsights = await import('applicationinsights'); let client: TelemetryClient; if (appInsights.defaultClient) { @@ -29,7 +29,7 @@ async function getClient(aiKey: string): Promise<TelemetryClient> { } if (aiKey.indexOf('AIF-') === 0) { - client.config.endpointUrl = 'https://vortex.data.microsoft.com/collect/v1'; + client.config.endpointUrl = testCollector ? 'https://mobile.events.data.microsoft.com/OneCollector/1.0' : 'https://vortex.data.microsoft.com/collect/v1'; } return client; } @@ -44,6 +44,8 @@ export class AppInsightsAppender implements ITelemetryAppender { private _eventPrefix: string, private _defaultData: { [key: string]: any } | null, aiKeyOrClientFactory: string | (() => TelemetryClient), // allow factory function for testing + private readonly testCollector?: boolean, + private readonly mirrored?: boolean ) { if (!this._defaultData) { this._defaultData = Object.create(null); @@ -68,7 +70,7 @@ export class AppInsightsAppender implements ITelemetryAppender { } if (!this._asyncAIClient) { - this._asyncAIClient = getClient(this._aiClient); + this._asyncAIClient = getClient(this._aiClient, this.testCollector ?? false); } this._asyncAIClient.then( @@ -89,6 +91,10 @@ export class AppInsightsAppender implements ITelemetryAppender { data = mixin(data, this._defaultData); data = validateTelemetryData(data); + if (this.testCollector) { + data.properties['common.useragent'] = this.mirrored ? 'mirror-collector++' : 'collector++'; + } + this._withAIClient((aiClient) => aiClient.trackEvent({ name: this._eventPrefix + '/' + eventName, properties: data.properties, diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 833f556f807..1770760c793 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -25,6 +25,9 @@ export const enum TerminalSettingId { AutomationShellLinux = 'terminal.integrated.automationShell.linux', AutomationShellMacOs = 'terminal.integrated.automationShell.osx', AutomationShellWindows = 'terminal.integrated.automationShell.windows', + AutomationProfileLinux = 'terminal.integrated.automationProfile.linux', + AutomationProfileMacOs = 'terminal.integrated.automationProfile.osx', + AutomationProfileWindows = 'terminal.integrated.automationProfile.windows', ShellArgsLinux = 'terminal.integrated.shellArgs.linux', ShellArgsMacOs = 'terminal.integrated.shellArgs.osx', ShellArgsWindows = 'terminal.integrated.shellArgs.windows', @@ -181,7 +184,12 @@ export const IPtyService = createDecorator<IPtyService>('ptyService'); export const enum ProcessPropertyType { Cwd = 'cwd', InitialCwd = 'initialCwd', - FixedDimensions = 'fixedDimensions' + FixedDimensions = 'fixedDimensions', + Title = 'title', + ShellType = 'shellType', + HasChildProcesses = 'hasChildProcesses', + ResolvedShellLaunchConfig = 'resolvedShellLaunchConfig', + OverrideDimensions = 'overrideDimensions' } export interface IProcessProperty<T extends ProcessPropertyType> { @@ -192,7 +200,12 @@ export interface IProcessProperty<T extends ProcessPropertyType> { export interface IProcessPropertyMap { [ProcessPropertyType.Cwd]: string, [ProcessPropertyType.InitialCwd]: string, - [ProcessPropertyType.FixedDimensions]: IFixedTerminalDimensions + [ProcessPropertyType.FixedDimensions]: IFixedTerminalDimensions, + [ProcessPropertyType.Title]: string + [ProcessPropertyType.ShellType]: TerminalShellType | undefined, + [ProcessPropertyType.HasChildProcesses]: boolean, + [ProcessPropertyType.ResolvedShellLaunchConfig]: IShellLaunchConfig, + [ProcessPropertyType.OverrideDimensions]: ITerminalDimensionsOverride | undefined } export interface IFixedTerminalDimensions { @@ -218,17 +231,12 @@ export interface IPtyService { readonly onPtyHostRequestResolveVariables?: Event<IRequestResolveVariablesEvent>; readonly onProcessData: Event<{ id: number, event: IProcessDataEvent | string }>; - readonly onProcessExit: Event<{ id: number, event: number | undefined }>; - readonly onProcessReady: Event<{ id: number, event: { pid: number, cwd: string, capabilities: ProcessCapability[] } }>; - readonly onProcessTitleChanged: Event<{ id: number, event: string }>; - readonly onProcessShellTypeChanged: Event<{ id: number, event: TerminalShellType }>; - readonly onProcessOverrideDimensions: Event<{ id: number, event: ITerminalDimensionsOverride | undefined }>; - readonly onProcessResolvedShellLaunchConfig: Event<{ id: number, event: IShellLaunchConfig }>; + readonly onProcessReady: Event<{ id: number, event: IProcessReadyEvent }>; readonly onProcessReplay: Event<{ id: number, event: IPtyHostProcessReplayEvent }>; readonly onProcessOrphanQuestion: Event<{ id: number }>; readonly onDidRequestDetach: Event<{ requestId: number, workspaceId: string, instanceId: number }>; - readonly onProcessDidChangeHasChildProcesses: Event<{ id: number, event: boolean }>; readonly onDidChangeProperty: Event<{ id: number, property: IProcessProperty<any> }> + readonly onProcessExit: Event<{ id: number, event: number | undefined }>; restartPtyHost?(): Promise<void>; shutdownAll?(): Promise<void>; @@ -527,14 +535,9 @@ export interface ITerminalChildProcess { capabilities: ProcessCapability[]; onProcessData: Event<IProcessDataEvent | string>; - onProcessExit: Event<number | undefined>; onProcessReady: Event<IProcessReadyEvent>; - onProcessTitleChanged: Event<string>; - onProcessShellTypeChanged: Event<TerminalShellType>; - onProcessOverrideDimensions?: Event<ITerminalDimensionsOverride | undefined>; - onProcessResolvedShellLaunchConfig?: Event<IShellLaunchConfig>; - onDidChangeHasChildProcesses?: Event<boolean>; onDidChangeProperty: Event<IProcessProperty<any>>; + onProcessExit: Event<number | undefined>; /** * Starts the process. diff --git a/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts b/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts index af012cffc67..9ea5b9acee8 100644 --- a/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts +++ b/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts @@ -73,6 +73,9 @@ const terminalProfileSchema: IJSONSchema = { const shellDeprecationMessageLinux = localize('terminal.integrated.shell.linux.deprecation', "This is deprecated, the new recommended way to configure your default shell is by creating a terminal profile in {0} and setting its profile name as the default in {1}. This will currently take priority over the new profiles settings but that will change in the future.", '`#terminal.integrated.profiles.linux#`', '`#terminal.integrated.defaultProfile.linux#`'); const shellDeprecationMessageOsx = localize('terminal.integrated.shell.osx.deprecation', "This is deprecated, the new recommended way to configure your default shell is by creating a terminal profile in {0} and setting its profile name as the default in {1}. This will currently take priority over the new profiles settings but that will change in the future.", '`#terminal.integrated.profiles.osx#`', '`#terminal.integrated.defaultProfile.osx#`'); const shellDeprecationMessageWindows = localize('terminal.integrated.shell.windows.deprecation', "This is deprecated, the new recommended way to configure your default shell is by creating a terminal profile in {0} and setting its profile name as the default in {1}. This will currently take priority over the new profiles settings but that will change in the future.", '`#terminal.integrated.profiles.windows#`', '`#terminal.integrated.defaultProfile.windows#`'); +const automationShellDeprecationMessageLinux = localize('terminal.integrated.automationShell.linux.deprecation', "This is deprecated, the new recommended way to configure your automation shell is by creating a terminal automation profile with {0}. This will currently take priority over the new automation profile settings but that will change in the future.", '`#terminal.integrated.automationProfile.linux#`'); +const automationShellDeprecationMessageOsx = localize('terminal.integrated.automationShell.osx.deprecation', "This is deprecated, the new recommended way to configure your automation shell is by creating a terminal automation profile with {0}. This will currently take priority over the new automation profile settings but that will change in the future.", '`#terminal.integrated.automationProfile.osx#`'); +const automationShellDeprecationMessageWindows = localize('terminal.integrated.automationShell.windows.deprecation', "This is deprecated, the new recommended way to configure your automation shell is by creating a terminal automation profile with {0}. This will currently take priority over the new automation profile settings but that will change in the future.", '`#terminal.integrated.automationProfile.windows#`'); const terminalPlatformConfiguration: IConfigurationNode = { id: 'terminal', @@ -87,7 +90,8 @@ const terminalPlatformConfiguration: IConfigurationNode = { comment: ['{0} and {1} are the `shell` and `shellArgs` settings keys'] }, "A path that when set will override {0} and ignore {1} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.linux`', '`shellArgs`'), type: ['string', 'null'], - default: null + default: null, + markdownDeprecationMessage: automationShellDeprecationMessageLinux }, [TerminalSettingId.AutomationShellMacOs]: { restricted: true, @@ -96,7 +100,8 @@ const terminalPlatformConfiguration: IConfigurationNode = { comment: ['{0} and {1} are the `shell` and `shellArgs` settings keys'] }, "A path that when set will override {0} and ignore {1} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.osx`', '`shellArgs`'), type: ['string', 'null'], - default: null + default: null, + markdownDeprecationMessage: automationShellDeprecationMessageOsx }, [TerminalSettingId.AutomationShellWindows]: { restricted: true, @@ -105,7 +110,38 @@ const terminalPlatformConfiguration: IConfigurationNode = { comment: ['{0} and {1} are the `shell` and `shellArgs` settings keys'] }, "A path that when set will override {0} and ignore {1} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.windows`', '`shellArgs`'), type: ['string', 'null'], - default: null + default: null, + markdownDeprecationMessage: automationShellDeprecationMessageWindows + }, + [TerminalSettingId.AutomationProfileLinux]: { + restricted: true, + markdownDescription: localize('terminal.integrated.automationProfile.linux', "The terminal profile to use on Linux for automation-related terminal usage like tasks and debug. This setting will currently be ignored if {0} is set.", '#terminal.integrated.automationShell.linux#'), + type: ['object', 'null'], + default: null, + 'anyOf': [ + { type: 'null' }, + terminalProfileSchema + ] + }, + [TerminalSettingId.AutomationProfileMacOs]: { + restricted: true, + description: localize('terminal.integrated.automationProfile.osx', "The terminal profile to use on macOS for automation-related terminal usage like tasks and debug. This setting will currently be ignored if {0} is set.", '#terminal.integrated.automationShell.osx#'), + type: ['object', 'null'], + default: null, + 'anyOf': [ + { type: 'null' }, + terminalProfileSchema + ] + }, + [TerminalSettingId.AutomationProfileWindows]: { + restricted: true, + description: localize('terminal.integrated.automationProfile.windows', "The terminal profile to use for automation-related terminal usage like tasks and debug. This setting will currently be ignored if {0} is set.", '#terminal.integrated.automationShell.windows#'), + type: ['object', 'null'], + default: null, + 'anyOf': [ + { type: 'null' }, + terminalProfileSchema + ] }, [TerminalSettingId.ShellLinux]: { restricted: true, diff --git a/src/vs/platform/terminal/common/terminalProfiles.ts b/src/vs/platform/terminal/common/terminalProfiles.ts index 63f49db9bd1..970db1c3b28 100644 --- a/src/vs/platform/terminal/common/terminalProfiles.ts +++ b/src/vs/platform/terminal/common/terminalProfiles.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from 'vs/base/common/codicons'; -import { IExtensionTerminalProfile, ITerminalProfile } from 'vs/platform/terminal/common/terminal'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { IExtensionTerminalProfile, ITerminalProfile, TerminalIcon } from 'vs/platform/terminal/common/terminal'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; export function createProfileSchemaEnums(detectedProfiles: ITerminalProfile[], extensionProfiles?: readonly IExtensionTerminalProfile[]): { @@ -56,3 +57,60 @@ function createExtensionProfileDescription(profile: IExtensionTerminalProfile): let description = `$(${ThemeIcon.isThemeIcon(profile.icon) ? profile.icon.id : profile.icon ? profile.icon : Codicon.terminal.id}) ${profile.title}\n- extensionIdenfifier: ${profile.extensionIdentifier}`; return description; } + + +export function terminalProfileArgsMatch(args1: string | string[] | undefined, args2: string | string[] | undefined): boolean { + if (!args1 && !args2) { + return true; + } else if (typeof args1 === 'string' && typeof args2 === 'string') { + return args1 === args2; + } else if (Array.isArray(args1) && Array.isArray(args2)) { + if (args1.length !== args2.length) { + return false; + } + for (let i = 0; i < args1.length; i++) { + if (args1[i] !== args2[i]) { + return false; + } + } + return true; + } + return false; +} + +export function terminalIconsEqual(iconOne?: TerminalIcon, iconTwo?: TerminalIcon): boolean { + if (!iconOne && !iconTwo) { + return true; + } else if (!iconOne || !iconTwo) { + return false; + } + + if (ThemeIcon.isThemeIcon(iconOne) && ThemeIcon.isThemeIcon(iconTwo)) { + return iconOne.id === iconTwo.id && iconOne.color === iconTwo.color; + } + if (typeof iconOne === 'object' && iconOne && 'light' in iconOne && 'dark' in iconOne + && typeof iconTwo === 'object' && iconTwo && 'light' in iconTwo && 'dark' in iconTwo) { + const castedIcon = (iconOne as { light: unknown, dark: unknown }); + const castedIconTwo = (iconTwo as { light: unknown, dark: unknown }); + if ((URI.isUri(castedIcon.light) || isUriComponents(castedIcon.light)) && (URI.isUri(castedIcon.dark) || isUriComponents(castedIcon.dark)) + && (URI.isUri(castedIconTwo.light) || isUriComponents(castedIconTwo.light)) && (URI.isUri(castedIconTwo.dark) || isUriComponents(castedIconTwo.dark))) { + return castedIcon.light.path === castedIconTwo.light.path && castedIcon.dark.path === castedIconTwo.dark.path; + } + } + if ((URI.isUri(iconOne) && URI.isUri(iconTwo)) || (isUriComponents(iconOne) || isUriComponents(iconTwo))) { + const castedIcon = (iconOne as { scheme: unknown, path: unknown }); + const castedIconTwo = (iconTwo as { scheme: unknown, path: unknown }); + return castedIcon.path === castedIconTwo.path && castedIcon.scheme === castedIconTwo.scheme; + } + + return false; +} + + +export function isUriComponents(thing: unknown): thing is UriComponents { + if (!thing) { + return false; + } + return typeof (<any>thing).path === 'string' && + typeof (<any>thing).scheme === 'string'; +} diff --git a/src/vs/platform/terminal/node/ptyHostService.ts b/src/vs/platform/terminal/node/ptyHostService.ts index e13e7730b6d..3c44bae9a5b 100644 --- a/src/vs/platform/terminal/node/ptyHostService.ts +++ b/src/vs/platform/terminal/node/ptyHostService.ts @@ -17,7 +17,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { LogLevelChannelClient } from 'vs/platform/log/common/logIpc'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { RequestStore } from 'vs/platform/terminal/common/requestStore'; -import { HeartbeatConstants, IHeartbeatService, IProcessDataEvent, IPtyService, IReconnectConstants, IRequestResolveVariablesEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalProfile, ITerminalsLayoutInfo, TerminalIcon, TerminalIpcChannels, IProcessProperty, TerminalShellType, TitleEventSource, ProcessPropertyType, ProcessCapability, IProcessPropertyMap, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { HeartbeatConstants, IHeartbeatService, IProcessDataEvent, IPtyService, IReconnectConstants, IRequestResolveVariablesEvent, IShellLaunchConfig, ITerminalLaunchError, ITerminalProfile, ITerminalsLayoutInfo, TerminalIcon, TerminalIpcChannels, IProcessProperty, TitleEventSource, ProcessPropertyType, ProcessCapability, IProcessPropertyMap, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { registerTerminalPlatformConfiguration } from 'vs/platform/terminal/common/terminalPlatformConfiguration'; import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; import { detectAvailableProfiles } from 'vs/platform/terminal/node/terminalProfiles'; @@ -64,28 +64,18 @@ export class PtyHostService extends Disposable implements IPtyService { private readonly _onProcessData = this._register(new Emitter<{ id: number, event: IProcessDataEvent | string }>()); readonly onProcessData = this._onProcessData.event; - private readonly _onProcessExit = this._register(new Emitter<{ id: number, event: number | undefined }>()); - readonly onProcessExit = this._onProcessExit.event; private readonly _onProcessReady = this._register(new Emitter<{ id: number, event: { pid: number, cwd: string, capabilities: ProcessCapability[] } }>()); readonly onProcessReady = this._onProcessReady.event; private readonly _onProcessReplay = this._register(new Emitter<{ id: number, event: IPtyHostProcessReplayEvent }>()); readonly onProcessReplay = this._onProcessReplay.event; - private readonly _onProcessTitleChanged = this._register(new Emitter<{ id: number, event: string }>()); - readonly onProcessTitleChanged = this._onProcessTitleChanged.event; - private readonly _onProcessShellTypeChanged = this._register(new Emitter<{ id: number, event: TerminalShellType }>()); - readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; - private readonly _onProcessOverrideDimensions = this._register(new Emitter<{ id: number, event: ITerminalDimensionsOverride | undefined }>()); - readonly onProcessOverrideDimensions = this._onProcessOverrideDimensions.event; - private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter<{ id: number, event: IShellLaunchConfig }>()); - readonly onProcessResolvedShellLaunchConfig = this._onProcessResolvedShellLaunchConfig.event; private readonly _onProcessOrphanQuestion = this._register(new Emitter<{ id: number }>()); readonly onProcessOrphanQuestion = this._onProcessOrphanQuestion.event; private readonly _onDidRequestDetach = this._register(new Emitter<{ requestId: number, workspaceId: string, instanceId: number }>()); readonly onDidRequestDetach = this._onDidRequestDetach.event; - private readonly _onProcessDidChangeHasChildProcesses = this._register(new Emitter<{ id: number, event: boolean }>()); - readonly onProcessDidChangeHasChildProcesses = this._onProcessDidChangeHasChildProcesses.event; private readonly _onDidChangeProperty = this._register(new Emitter<{ id: number, property: IProcessProperty<any> }>()); readonly onDidChangeProperty = this._onDidChangeProperty.event; + private readonly _onProcessExit = this._register(new Emitter<{ id: number, event: number | undefined }>()); + readonly onProcessExit = this._onProcessExit.event; constructor( private readonly _reconnectConstants: IReconnectConstants, @@ -202,13 +192,8 @@ export class PtyHostService extends Disposable implements IPtyService { // Create proxy and forward events const proxy = ProxyChannel.toService<IPtyService>(client.getChannel(TerminalIpcChannels.PtyHost)); this._register(proxy.onProcessData(e => this._onProcessData.fire(e))); - this._register(proxy.onProcessExit(e => this._onProcessExit.fire(e))); this._register(proxy.onProcessReady(e => this._onProcessReady.fire(e))); - this._register(proxy.onProcessTitleChanged(e => this._onProcessTitleChanged.fire(e))); - this._register(proxy.onProcessShellTypeChanged(e => this._onProcessShellTypeChanged.fire(e))); - this._register(proxy.onProcessOverrideDimensions(e => this._onProcessOverrideDimensions.fire(e))); - this._register(proxy.onProcessResolvedShellLaunchConfig(e => this._onProcessResolvedShellLaunchConfig.fire(e))); - this._register(proxy.onProcessDidChangeHasChildProcesses(e => this._onProcessDidChangeHasChildProcesses.fire(e))); + this._register(proxy.onProcessExit(e => this._onProcessExit.fire(e))); this._register(proxy.onDidChangeProperty(e => this._onDidChangeProperty.fire(e))); this._register(proxy.onProcessReplay(e => this._onProcessReplay.fire(e))); this._register(proxy.onProcessOrphanQuestion(e => this._onProcessOrphanQuestion.fire(e))); diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index 5d2ae38fc73..16c68b790de 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -12,7 +12,7 @@ import { URI } from 'vs/base/common/uri'; import { getSystemShell } from 'vs/base/node/shell'; import { ILogService } from 'vs/platform/log/common/log'; import { RequestStore } from 'vs/platform/terminal/common/requestStore'; -import { IProcessDataEvent, IProcessReadyEvent, IPtyService, IRawTerminalInstanceLayoutInfo, IReconnectConstants, IRequestResolveVariablesEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalInstanceLayoutInfoById, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalTabLayoutInfoById, TerminalIcon, IProcessProperty, TerminalShellType, TitleEventSource, ProcessPropertyType, ProcessCapability, IProcessPropertyMap, IFixedTerminalDimensions } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IProcessReadyEvent, IPtyService, IRawTerminalInstanceLayoutInfo, IReconnectConstants, IRequestResolveVariablesEvent, IShellLaunchConfig, ITerminalInstanceLayoutInfoById, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalTabLayoutInfoById, TerminalIcon, IProcessProperty, TitleEventSource, ProcessPropertyType, IProcessPropertyMap, IFixedTerminalDimensions, ProcessCapability } from 'vs/platform/terminal/common/terminal'; import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; import { escapeNonWindowsPath } from 'vs/platform/terminal/common/terminalEnvironment'; import { Terminal as XtermTerminal } from 'xterm-headless'; @@ -44,24 +44,14 @@ export class PtyService extends Disposable implements IPtyService { readonly onProcessData = this._onProcessData.event; private readonly _onProcessReplay = this._register(new Emitter<{ id: number, event: IPtyHostProcessReplayEvent }>()); readonly onProcessReplay = this._onProcessReplay.event; - private readonly _onProcessExit = this._register(new Emitter<{ id: number, event: number | undefined }>()); - readonly onProcessExit = this._onProcessExit.event; private readonly _onProcessReady = this._register(new Emitter<{ id: number, event: { pid: number, cwd: string, capabilities: ProcessCapability[] } }>()); readonly onProcessReady = this._onProcessReady.event; - private readonly _onProcessTitleChanged = this._register(new Emitter<{ id: number, event: string }>()); - readonly onProcessTitleChanged = this._onProcessTitleChanged.event; - private readonly _onProcessShellTypeChanged = this._register(new Emitter<{ id: number, event: TerminalShellType }>()); - readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; - private readonly _onProcessOverrideDimensions = this._register(new Emitter<{ id: number, event: ITerminalDimensionsOverride | undefined }>()); - readonly onProcessOverrideDimensions = this._onProcessOverrideDimensions.event; - private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter<{ id: number, event: IShellLaunchConfig }>()); - readonly onProcessResolvedShellLaunchConfig = this._onProcessResolvedShellLaunchConfig.event; + private readonly _onProcessExit = this._register(new Emitter<{ id: number, event: number | undefined }>()); + readonly onProcessExit = this._onProcessExit.event; private readonly _onProcessOrphanQuestion = this._register(new Emitter<{ id: number }>()); readonly onProcessOrphanQuestion = this._onProcessOrphanQuestion.event; private readonly _onDidRequestDetach = this._register(new Emitter<{ requestId: number, workspaceId: string, instanceId: number }>()); readonly onDidRequestDetach = this._onDidRequestDetach.event; - private readonly _onProcessDidChangeHasChildProcesses = this._register(new Emitter<{ id: number, event: boolean }>()); - readonly onProcessDidChangeHasChildProcesses = this._onProcessDidChangeHasChildProcesses.event; private readonly _onDidChangeProperty = this._register(new Emitter<{ id: number, property: IProcessProperty<any> }>()); readonly onDidChangeProperty = this._onDidChangeProperty.event; @@ -195,31 +185,20 @@ export class PtyService extends Disposable implements IPtyService { const id = ++this._lastPtyId; const process = new TerminalProcess(shellLaunchConfig, cwd, cols, rows, env, executableEnv, windowsEnableConpty, this._logService); process.onProcessData(event => this._onProcessData.fire({ id, event })); - process.onProcessExit(event => this._onProcessExit.fire({ id, event })); - if (process.onProcessOverrideDimensions) { - process.onProcessOverrideDimensions(event => this._onProcessOverrideDimensions.fire({ id, event })); - } - if (process.onProcessResolvedShellLaunchConfig) { - process.onProcessResolvedShellLaunchConfig(event => this._onProcessResolvedShellLaunchConfig.fire({ id, event })); - } - if (process.onDidChangeHasChildProcesses) { - process.onDidChangeHasChildProcesses(event => this._onProcessDidChangeHasChildProcesses.fire({ id, event })); - } const processLaunchOptions: IPersistentTerminalProcessLaunchOptions = { env, executableEnv, windowsEnableConpty }; const persistentProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, shouldPersist, cols, rows, processLaunchOptions, unicodeVersion, this._reconnectConstants, this._logService, isReviving ? shellLaunchConfig.initialText : undefined, shellLaunchConfig.icon, shellLaunchConfig.color, shellLaunchConfig.fixedDimensions); - process.onProcessExit(() => { + process.onDidChangeProperty(property => this._onDidChangeProperty.fire({ id, property })); + process.onProcessExit(event => { persistentProcess.dispose(); this._ptys.delete(id); + this._onProcessExit.fire({ id, event }); }); - process.onDidChangeProperty(property => this._onDidChangeProperty.fire({ id, property })); persistentProcess.onProcessReplay(event => this._onProcessReplay.fire({ id, event })); persistentProcess.onProcessReady(event => this._onProcessReady.fire({ id, event })); - persistentProcess.onProcessTitleChanged(event => this._onProcessTitleChanged.fire({ id, event })); - persistentProcess.onProcessShellTypeChanged(event => this._onProcessShellTypeChanged.fire({ id, event })); persistentProcess.onProcessOrphanQuestion(() => this._onProcessOrphanQuestion.fire({ id })); persistentProcess.onDidChangeProperty(property => this._onDidChangeProperty.fire({ id, property })); this._ptys.set(id, persistentProcess); @@ -428,12 +407,6 @@ export class PersistentTerminalProcess extends Disposable { readonly onProcessReplay = this._onProcessReplay.event; private readonly _onProcessReady = this._register(new Emitter<IProcessReadyEvent>()); readonly onProcessReady = this._onProcessReady.event; - private readonly _onProcessTitleChanged = this._register(new Emitter<string>()); - readonly onProcessTitleChanged = this._onProcessTitleChanged.event; - private readonly _onProcessShellTypeChanged = this._register(new Emitter<TerminalShellType>()); - readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; - private readonly _onProcessOverrideDimensions = this._register(new Emitter<ITerminalDimensionsOverride | undefined>()); - readonly onProcessOverrideDimensions = this._onProcessOverrideDimensions.event; private readonly _onProcessData = this._register(new Emitter<string>()); readonly onProcessData = this._onProcessData.event; private readonly _onProcessOrphanQuestion = this._register(new Emitter<void>()); @@ -511,20 +484,19 @@ export class PersistentTerminalProcess extends Disposable { this._logService.info(`Persistent process "${this._persistentProcessId}": The short reconnection grace time of ${printTime(reconnectConstants.shortGraceTime)} has expired, shutting down pid ${this._pid}`); this.shutdown(true); }, reconnectConstants.shortGraceTime)); - + this._register(this._terminalProcess.onProcessExit(() => this._bufferer.stopBuffering(this._persistentProcessId))); this._register(this._terminalProcess.onProcessReady(e => { this._pid = e.pid; this._cwd = e.cwd; this._onProcessReady.fire(e); })); - this._register(this._terminalProcess.onProcessTitleChanged(e => this._onProcessTitleChanged.fire(e))); - this._register(this._terminalProcess.onProcessShellTypeChanged(e => this._onProcessShellTypeChanged.fire(e))); - this._register(this._terminalProcess.onDidChangeProperty(e => this._onDidChangeProperty.fire(e))); + this._register(this._terminalProcess.onDidChangeProperty(e => { + this._onDidChangeProperty.fire(e); + })); // Data buffering to reduce the amount of messages going to the renderer this._bufferer = new TerminalDataBufferer((_, data) => this._onProcessData.fire(data)); this._register(this._bufferer.startBuffering(this._persistentProcessId, this._terminalProcess.onProcessData)); - this._register(this._terminalProcess.onProcessExit(() => this._bufferer.stopBuffering(this._persistentProcessId))); // Data recording for reconnect this._register(this.onProcessData(e => this._serializer.handleData(e))); @@ -579,8 +551,8 @@ export class PersistentTerminalProcess extends Disposable { } } else { this._onProcessReady.fire({ pid: this._pid, cwd: this._cwd, capabilities: this._terminalProcess.capabilities, requiresWindowsMode: isWindows && getWindowsBuildNumber() < 21376 }); - this._onProcessTitleChanged.fire(this._terminalProcess.currentTitle); - this._onProcessShellTypeChanged.fire(this._terminalProcess.shellType); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.Title, value: this._terminalProcess.currentTitle }); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.ShellType, value: this._terminalProcess.shellType }); this.triggerReplay(); } return undefined; diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index 8fa92e037ed..af6a0f85403 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -14,7 +14,7 @@ import { URI } from 'vs/base/common/uri'; import { Promises } from 'vs/base/node/pfs'; import { localize } from 'vs/nls'; import { ILogService } from 'vs/platform/log/common/log'; -import { FlowControlConstants, IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalLaunchError, IProcessProperty, IProcessPropertyMap as IProcessPropertyMap, ProcessPropertyType, TerminalShellType, ProcessCapability } from 'vs/platform/terminal/common/terminal'; +import { FlowControlConstants, IShellLaunchConfig, ITerminalChildProcess, ITerminalLaunchError, IProcessProperty, IProcessPropertyMap as IProcessPropertyMap, ProcessPropertyType, TerminalShellType, ProcessCapability, IProcessReadyEvent } from 'vs/platform/terminal/common/terminal'; import { ChildProcessMonitor } from 'vs/platform/terminal/node/childProcessMonitor'; import { findExecutable, getWindowsBuildNumber } from 'vs/platform/terminal/node/terminalEnvironment'; import { WindowsShellHelper } from 'vs/platform/terminal/node/windowsShellHelper'; @@ -79,7 +79,12 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private _properties: IProcessPropertyMap = { cwd: '', initialCwd: '', - fixedDimensions: { cols: undefined, rows: undefined } + fixedDimensions: { cols: undefined, rows: undefined }, + title: '', + shellType: undefined, + hasChildProcesses: true, + resolvedShellLaunchConfig: {}, + overrideDimensions: undefined }; private static _lastKillOrStart = 0; private _exitCode: number | undefined; @@ -110,21 +115,12 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private readonly _onProcessData = this._register(new Emitter<string>()); readonly onProcessData = this._onProcessData.event; - private readonly _onProcessExit = this._register(new Emitter<number>()); - readonly onProcessExit = this._onProcessExit.event; private readonly _onProcessReady = this._register(new Emitter<IProcessReadyEvent>()); readonly onProcessReady = this._onProcessReady.event; - private readonly _onProcessTitleChanged = this._register(new Emitter<string>()); - readonly onProcessTitleChanged = this._onProcessTitleChanged.event; - private readonly _onProcessShellTypeChanged = this._register(new Emitter<TerminalShellType>()); - readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; - private readonly _onDidChangeHasChildProcesses = this._register(new Emitter<boolean>()); - readonly onDidChangeHasChildProcesses = this._onDidChangeHasChildProcesses.event; private readonly _onDidChangeProperty = this._register(new Emitter<IProcessProperty<any>>()); readonly onDidChangeProperty = this._onDidChangeProperty.event; - - onProcessOverrideDimensions?: Event<ITerminalDimensionsOverride | undefined> | undefined; - onProcessResolvedShellLaunchConfig?: Event<IShellLaunchConfig> | undefined; + private readonly _onProcessExit = this._register(new Emitter<number>()); + readonly onProcessExit = this._onProcessExit.event; constructor( readonly shellLaunchConfig: IShellLaunchConfig, @@ -178,8 +174,8 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess // WindowsShellHelper is used to fetch the process title and shell type this.onProcessReady(e => { this._windowsShellHelper = this._register(new WindowsShellHelper(e.pid)); - this._register(this._windowsShellHelper.onShellTypeChanged(e => this._onProcessShellTypeChanged.fire(e))); - this._register(this._windowsShellHelper.onShellNameChanged(e => this._onProcessTitleChanged.fire(e))); + this._register(this._windowsShellHelper.onShellTypeChanged(e => this._onDidChangeProperty.fire({ type: ProcessPropertyType.ShellType, value: e }))); + this._register(this._windowsShellHelper.onShellNameChanged(e => this._onDidChangeProperty.fire({ type: ProcessPropertyType.Title, value: e }))); }); } // Enable the cwd detection capability if the process supports it @@ -253,7 +249,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess const ptyProcess = (await import('node-pty')).spawn(shellLaunchConfig.executable!, args, options); this._ptyProcess = ptyProcess; this._childProcessMonitor = this._register(new ChildProcessMonitor(ptyProcess.pid, this._logService)); - this._childProcessMonitor.onDidChangeHasChildProcesses(this._onDidChangeHasChildProcesses.fire, this._onDidChangeHasChildProcesses); + this._childProcessMonitor.onDidChangeHasChildProcesses(value => this._onDidChangeProperty.fire({ type: ProcessPropertyType.HasChildProcesses, value })); this._processStartupComplete = new Promise<void>(c => { this.onProcessReady(() => c()); }); @@ -360,7 +356,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess return; } this._currentTitle = ptyProcess.process; - this._onProcessTitleChanged.fire(this._currentTitle); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.Title, value: this._currentTitle }); } shutdown(immediate: boolean): void { @@ -402,21 +398,31 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } async refreshProperty<T extends ProcessPropertyType>(type: ProcessPropertyType): Promise<IProcessPropertyMap[T]> { - if (type === ProcessPropertyType.Cwd) { - const newCwd = await this.getCwd(); - if (newCwd !== this._properties.cwd) { - this._properties.cwd = newCwd; - this._onDidChangeProperty.fire({ type: ProcessPropertyType.Cwd, value: this._properties.cwd }); - } - return newCwd; - } else { - return this.getInitialCwd(); + switch (type) { + case ProcessPropertyType.Cwd: + const newCwd = await this.getCwd(); + if (newCwd !== this._properties.cwd) { + this._properties.cwd = newCwd; + this._onDidChangeProperty.fire({ type: ProcessPropertyType.Cwd, value: this._properties.cwd }); + } + return newCwd as IProcessPropertyMap[T]; + case ProcessPropertyType.InitialCwd: + const initialCwd = await this.getInitialCwd(); + if (initialCwd !== this._properties.initialCwd) { + this._properties.initialCwd = initialCwd; + this._onDidChangeProperty.fire({ type: ProcessPropertyType.InitialCwd, value: this._properties.initialCwd }); + } + return initialCwd as IProcessPropertyMap[T]; + case ProcessPropertyType.Title: + return this.currentTitle as IProcessPropertyMap[T]; + default: + return this.shellType as IProcessPropertyMap[T]; } } async updateProperty<T extends ProcessPropertyType>(type: ProcessPropertyType, value: IProcessPropertyMap[T]): Promise<void> { //TODO: why is the type check necessary? - if (type === ProcessPropertyType.FixedDimensions && typeof value !== 'string') { + if (type === ProcessPropertyType.FixedDimensions && typeof value !== 'string' && value && ('cols' in value || 'rows' in value)) { this._properties.fixedDimensions = value; } } diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index 2da7bec19bb..5a19922b283 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -25,6 +25,16 @@ export interface ColorContribution { readonly deprecationMessage: string | undefined; } +/** + * Returns the css variable name for the given color identifier. Dots (`.`) are replaced with hyphens (`-`) and + * everything is prefixed with `--vscode-`. + * + * @sample `editorSuggestWidget.background` is `--vscode-editorSuggestWidget-background`. + */ +export function asCssVariableName(colorIdent: ColorIdentifier): string { + return `--vscode-${colorIdent.replace('.', '-')}`; +} + export const enum ColorTransformType { Darken, Lighten, diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 88a72a635a9..3806e892161 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -19,8 +19,7 @@ import { basename, join, normalize, posix } from 'vs/base/common/path'; import { getMarks, mark } from 'vs/base/common/performance'; import { IProcessEnvironment, isMacintosh, isWindows } from 'vs/base/common/platform'; import { cwd } from 'vs/base/common/process'; -import { extUriBiasedIgnorePathCase, normalizePath, originalFSPath, removeTrailingPathSeparator } from 'vs/base/common/resources'; -import { equalsIgnoreCase } from 'vs/base/common/strings'; +import { extUriBiasedIgnorePathCase, isEqualAuthority, normalizePath, originalFSPath, removeTrailingPathSeparator } from 'vs/base/common/resources'; import { assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; @@ -1198,7 +1197,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return false; } - return getRemoteAuthority(uri) === remoteAuthority; + return isEqualAuthority(getRemoteAuthority(uri), remoteAuthority); }); folderUris = folderUris.filter(folderUriStr => { @@ -1207,7 +1206,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return false; } - return folderUri ? getRemoteAuthority(folderUri) === remoteAuthority : false; + return folderUri ? isEqualAuthority(getRemoteAuthority(folderUri), remoteAuthority) : false; }); fileUris = fileUris.filter(fileUriStr => { @@ -1216,7 +1215,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return false; } - return fileUri ? getRemoteAuthority(fileUri) === remoteAuthority : false; + return fileUri ? isEqualAuthority(getRemoteAuthority(fileUri), remoteAuthority) : false; }); openConfig.cli._ = cliArgs; @@ -1477,7 +1476,3 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return this.getWindowById(browserWindow.id); } } - -function isEqualAuthority(a1: string | undefined, a2: string | undefined) { - return a1 === a2 || (a1 !== undefined && a2 !== undefined && equalsIgnoreCase(a1, a2)); -} diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index 4cb714335cd..9c7a5c2785f 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -12,7 +12,7 @@ import { normalizeDriveLetter } from 'vs/base/common/labels'; import { Schemas } from 'vs/base/common/network'; import { extname, isAbsolute } from 'vs/base/common/path'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; -import { extname as resourceExtname, extUriBiasedIgnorePathCase, IExtUri } from 'vs/base/common/resources'; +import { extname as resourceExtname, extUriBiasedIgnorePathCase, IExtUri, isEqualAuthority } from 'vs/base/common/resources'; import { URI, UriComponents } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -398,7 +398,7 @@ export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, const edits = jsonEdit.setProperty(rawWorkspaceContents, ['folders'], rewrittenFolders, formattingOptions); let newContent = jsonEdit.applyEdits(rawWorkspaceContents, edits); - if (storedWorkspace.remoteAuthority === getRemoteAuthority(targetConfigPathURI)) { + if (isEqualAuthority(storedWorkspace.remoteAuthority, getRemoteAuthority(targetConfigPathURI))) { // unsaved remote workspaces have the remoteAuthority set. Remove it when no longer nexessary. newContent = jsonEdit.applyEdits(newContent, jsonEdit.removeProperty(newContent, ['remoteAuthority'], formattingOptions)); } @@ -547,6 +547,7 @@ export function toStoreData(recents: IRecentlyOpened): RecentlyOpenedStorageData for (const recent of recents.files) { serialized.entries.push({ fileUri: recent.fileUri.toString(), label: recent.label, remoteAuthority: recent.remoteAuthority }); } + return serialized; } diff --git a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts index 63ecd2e5312..c5f4f948a43 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts @@ -41,11 +41,13 @@ export interface IWorkspacesHistoryMainService { export class WorkspacesHistoryMainService extends Disposable implements IWorkspacesHistoryMainService { - private static readonly MAX_TOTAL_RECENT_ENTRIES = 100; + private static readonly MAX_TOTAL_RECENT_ENTRIES = 500; private static readonly MAX_MACOS_DOCK_RECENT_WORKSPACES = 7; // prefer higher number of workspaces... private static readonly MAX_MACOS_DOCK_RECENT_ENTRIES_TOTAL = 10; // ...over number of files + private static readonly MAX_WINDOWS_JUMP_LIST_ENTRIES = 7; + // Exclude some very common files from the dock/taskbar private static readonly COMMON_FILES_FILTER = [ 'COMMIT_EDITMSG', @@ -98,21 +100,21 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa // Workspace if (isRecentWorkspace(recent)) { - if (!this.workspacesManagementMainService.isUntitledWorkspace(recent.workspace) && indexOfWorkspace(workspaces, recent.workspace) === -1) { + if (!this.workspacesManagementMainService.isUntitledWorkspace(recent.workspace) && this.indexOfWorkspace(workspaces, recent.workspace) === -1) { workspaces.push(recent); } } // Folder else if (isRecentFolder(recent)) { - if (indexOfFolder(workspaces, recent.folderUri) === -1) { + if (this.indexOfFolder(workspaces, recent.folderUri) === -1) { workspaces.push(recent); } } // File else { - const alreadyExistsInHistory = indexOfFile(files, recent.fileUri) >= 0; + const alreadyExistsInHistory = this.indexOfFile(files, recent.fileUri) >= 0; const shouldBeFiltered = recent.fileUri.scheme === Schemas.file && WorkspacesHistoryMainService.COMMON_FILES_FILTER.indexOf(basename(recent.fileUri)) >= 0; if (!alreadyExistsInHistory && !shouldBeFiltered) { @@ -147,7 +149,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa removeRecentlyOpened(recentToRemove: URI[]): void { const keep = (recent: IRecent) => { - const uri = location(recent); + const uri = this.location(recent); for (const resourceToRemove of recentToRemove) { if (extUriBiasedIgnorePathCase.isEqual(resourceToRemove, uri)) { return false; @@ -187,7 +189,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa const workspaceEntries: string[] = []; let entries = 0; for (let i = 0; i < mru.workspaces.length && entries < WorkspacesHistoryMainService.MAX_MACOS_DOCK_RECENT_WORKSPACES; i++) { - const loc = location(mru.workspaces[i]); + const loc = this.location(mru.workspaces[i]); if (loc.scheme === Schemas.file) { const workspacePath = originalFSPath(loc); if (await Promises.exists(workspacePath)) { @@ -200,7 +202,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa // Collect max-N recent files that are known to exist const fileEntries: string[] = []; for (let i = 0; i < mru.files.length && entries < WorkspacesHistoryMainService.MAX_MACOS_DOCK_RECENT_ENTRIES_TOTAL; i++) { - const loc = location(mru.files[i]); + const loc = this.location(mru.files[i]); if (loc.scheme === Schemas.file) { const filePath = originalFSPath(loc); if ( @@ -256,7 +258,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa if (currentFiles) { for (let currentFile of currentFiles) { const fileUri = currentFile.fileUri; - if (fileUri && indexOfFile(files, fileUri) === -1) { + if (fileUri && this.indexOfFile(files, fileUri) === -1) { files.push({ fileUri }); } } @@ -272,7 +274,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa // Get from storage let recents = this.getRecentlyOpenedFromStorage(); for (let recent of recents.workspaces) { - let index = isRecentFolder(recent) ? indexOfFolder(workspaces, recent.folderUri) : indexOfWorkspace(workspaces, recent.workspace); + let index = isRecentFolder(recent) ? this.indexOfFolder(workspaces, recent.folderUri) : this.indexOfWorkspace(workspaces, recent.workspace); if (index >= 0) { workspaces[index].label = workspaces[index].label || recent.label; } else { @@ -281,7 +283,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa } for (let recent of recents.files) { - let index = indexOfFile(files, recent.fileUri); + let index = this.indexOfFile(files, recent.fileUri); if (index >= 0) { files[index].label = files[index].label || recent.label; } else { @@ -346,7 +348,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa // Add entries let hasWorkspaces = false; - const items: JumpListItem[] = coalesce(this.getRecentlyOpened().workspaces.slice(0, 7 /* limit number of entries here */).map(recent => { + const items: JumpListItem[] = coalesce(this.getRecentlyOpened().workspaces.slice(0, WorkspacesHistoryMainService.MAX_WINDOWS_JUMP_LIST_ENTRIES).map(recent => { const workspace = isRecentWorkspace(recent) ? recent.workspace : recent.folderUri; const { title, description } = this.getWindowsJumpListLabel(workspace, recent.label); @@ -399,7 +401,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa // Single Folder if (URI.isUri(workspace)) { - return { title: basename(workspace), description: renderJumpListPathDescription(workspace) }; + return { title: basename(workspace), description: this.renderJumpListPathDescription(workspace) }; } // Workspace: Untitled @@ -413,34 +415,34 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1); } - return { title: localize('workspaceName', "{0} (Workspace)", filename), description: renderJumpListPathDescription(workspace.configPath) }; - } -} - -function renderJumpListPathDescription(uri: URI) { - return uri.scheme === 'file' ? normalizeDriveLetter(uri.fsPath) : uri.toString(); -} - -function location(recent: IRecent): URI { - if (isRecentFolder(recent)) { - return recent.folderUri; + return { title: localize('workspaceName', "{0} (Workspace)", filename), description: this.renderJumpListPathDescription(workspace.configPath) }; } - if (isRecentFile(recent)) { - return recent.fileUri; + private renderJumpListPathDescription(uri: URI) { + return uri.scheme === 'file' ? normalizeDriveLetter(uri.fsPath) : uri.toString(); } - return recent.workspace.configPath; -} + private location(recent: IRecent): URI { + if (isRecentFolder(recent)) { + return recent.folderUri; + } -function indexOfWorkspace(arr: IRecent[], candidate: IWorkspaceIdentifier): number { - return arr.findIndex(workspace => isRecentWorkspace(workspace) && workspace.workspace.id === candidate.id); -} + if (isRecentFile(recent)) { + return recent.fileUri; + } -function indexOfFolder(arr: IRecent[], candidate: URI): number { - return arr.findIndex(folder => isRecentFolder(folder) && extUriBiasedIgnorePathCase.isEqual(folder.folderUri, candidate)); -} + return recent.workspace.configPath; + } -function indexOfFile(arr: IRecentFile[], candidate: URI): number { - return arr.findIndex(file => extUriBiasedIgnorePathCase.isEqual(file.fileUri, candidate)); + private indexOfWorkspace(arr: IRecent[], candidate: IWorkspaceIdentifier): number { + return arr.findIndex(workspace => isRecentWorkspace(workspace) && workspace.workspace.id === candidate.id); + } + + private indexOfFolder(arr: IRecent[], candidate: URI): number { + return arr.findIndex(folder => isRecentFolder(folder) && extUriBiasedIgnorePathCase.isEqual(folder.folderUri, candidate)); + } + + private indexOfFile(arr: IRecentFile[], candidate: URI): number { + return arr.findIndex(file => extUriBiasedIgnorePathCase.isEqual(file.fileUri, candidate)); + } } diff --git a/src/vs/server/remoteCli.ts b/src/vs/server/remoteCli.ts index 0cdcf016205..fcc82d710ae 100644 --- a/src/vs/server/remoteCli.ts +++ b/src/vs/server/remoteCli.ts @@ -16,6 +16,15 @@ import { createWaitMarkerFile } from 'vs/platform/environment/node/wait'; import { PipeCommand } from 'vs/workbench/api/node/extHostCLIServer'; import { hasStdinWithoutTty, getStdinFilePath, readFromStdin } from 'vs/platform/environment/node/stdin'; +/* + * Implements a standalone CLI app that opens VS Code from a remote terminal. + * - In integrated terminals for remote windows this connects to the remote server though a pipe. + * The pipe is passed in env VSCODE_IPC_HOOK_CLI. + * - In external terminals for WSL this calls VS Code on the Windows side. + * The VS Code desktop executable path is passed in env VSCODE_CLIENT_COMMAND. + */ + + interface ProductDescription { productName: string; version: string; diff --git a/src/vs/server/remoteTerminalChannel.ts b/src/vs/server/remoteTerminalChannel.ts index dec98015031..2c31f0cd99e 100644 --- a/src/vs/server/remoteTerminalChannel.ts +++ b/src/vs/server/remoteTerminalChannel.ts @@ -128,6 +128,7 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel< case '$setTerminalLayoutInfo': return this._setTerminalLayoutInfo(<ISetTerminalLayoutInfoArgs>args); case '$serializeTerminalState': return this._ptyService.serializeTerminalState.apply(this._ptyService, args); case '$reviveTerminalProcesses': return this._ptyService.reviveTerminalProcesses.apply(this._ptyService, args); + case '$setUnicodeVersion': return this._ptyService.setUnicodeVersion.apply(this._ptyService, args); case '$reduceConnectionGraceTime': return this._reduceConnectionGraceTime(); case '$updateIcon': return this._ptyService.updateIcon.apply(this._ptyService, args); case '$updateTitle': return this._ptyService.updateTitle.apply(this._ptyService, args); @@ -148,15 +149,10 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel< case '$onPtyHostResponsiveEvent': return this._ptyService.onPtyHostResponsive || Event.None; case '$onPtyHostRequestResolveVariablesEvent': return this._ptyService.onPtyHostRequestResolveVariables || Event.None; case '$onProcessDataEvent': return this._ptyService.onProcessData; - case '$onProcessExitEvent': return this._ptyService.onProcessExit; case '$onProcessReadyEvent': return this._ptyService.onProcessReady; + case '$onProcessExitEvent': return this._ptyService.onProcessExit; case '$onProcessReplayEvent': return this._ptyService.onProcessReplay; - case '$onProcessTitleChangedEvent': return this._ptyService.onProcessTitleChanged; - case '$onProcessShellTypeChangedEvent': return this._ptyService.onProcessShellTypeChanged; - case '$onProcessOverrideDimensionsEvent': return this._ptyService.onProcessOverrideDimensions; - case '$onProcessResolvedShellLaunchConfigEvent': return this._ptyService.onProcessResolvedShellLaunchConfig; case '$onProcessOrphanQuestion': return this._ptyService.onProcessOrphanQuestion; - case '$onProcessDidChangeHasChildProcesses': return this._ptyService.onProcessDidChangeHasChildProcesses; case '$onExecuteCommand': return this.onExecuteCommand; case '$onDidRequestDetach': return this._ptyService.onDidRequestDetach || Event.None; case '$onDidChangeProperty': return this._ptyService.onDidChangeProperty; diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 39129a134cf..b4af49f2c96 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1826,6 +1826,7 @@ declare module 'vscode' { /** * The text of the hint. */ + // todo@API label? text: string; /** * The position of this hint. diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index cc7db0622e0..e213baa9dfb 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -138,7 +138,7 @@ export class MainThreadCommentThread implements modes.CommentThread { if (modified('range')) { this._range = changes.range!; } if (modified('label')) { this._label = changes.label; } - if (modified('contextValue')) { this._contextValue = changes.contextValue; } + if (modified('contextValue')) { this._contextValue = changes.contextValue === null ? undefined : changes.contextValue; } if (modified('comments')) { this._comments = changes.comments; } if (modified('collapseState')) { this._collapsibleState = changes.collapseState; } if (modified('canReply')) { this.canReply = changes.canReply!; } diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 68c5acf3c18..4e8435e598e 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -10,13 +10,13 @@ import { URI } from 'vs/base/common/uri'; import { StopWatch } from 'vs/base/common/stopwatch'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IShellLaunchConfig, IShellLaunchConfigDto, ITerminalDimensions, TerminalLocation, TitleEventSource } from 'vs/platform/terminal/common/terminal'; +import { IProcessProperty, IShellLaunchConfig, IShellLaunchConfigDto, ProcessPropertyType, TerminalLocation, TitleEventSource } from 'vs/platform/terminal/common/terminal'; import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; import { ITerminalEditorService, ITerminalExternalLinkProvider, ITerminalGroupService, ITerminalInstance, ITerminalInstanceService, ITerminalLink, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy'; import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { deserializeEnvironmentVariableCollection, serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; -import { IStartExtensionTerminalRequest, ITerminalProcessExtHostProxy, ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IStartExtensionTerminalRequest, ITerminalProcessExtHostProxy, ITerminalProfileResolverService, ITerminalProfileService } from 'vs/workbench/contrib/terminal/common/terminal'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { withNullAsUndefined } from 'vs/base/common/types'; import { OperatingSystem, OS } from 'vs/base/common/platform'; @@ -57,7 +57,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape @ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, - @ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService + @ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService, + @ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService ) { this._proxy = _extHostContext.getProxy(ExtHostContext.ExtHostTerminalService); @@ -97,7 +98,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._os = env?.os || OS; this._updateDefaultProfile(); }); - this._terminalService.onDidChangeAvailableProfiles(() => this._updateDefaultProfile()); + this._terminalProfileService.onDidChangeAvailableProfiles(() => this._updateDefaultProfile()); } public dispose(): void { @@ -141,8 +142,6 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape isExtensionOwnedTerminal: launchConfig.isExtensionOwnedTerminal, useShellEnvironment: launchConfig.useShellEnvironment, }; - // eslint-disable-next-line no-async-promise-executor - const terminal = Promises.withAsyncBody<ITerminalInstance>(async r => { const terminal = await this._terminalService.createTerminal({ config: shellLaunchConfig, @@ -191,6 +190,10 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape await instance?.sendText(text, addNewLine); } + public $sendProcessExit(terminalId: number, exitCode: number | undefined): void { + this._terminalProcessProxies.get(terminalId)?.emitExit(exitCode); + } + public $startSendingDataEvents(): void { if (!this._dataEventTracker) { this._dataEventTracker = this._instantiationService.createInstance(TerminalDataEventTracker, (id, data) => { @@ -224,7 +227,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape public $registerProfileProvider(id: string, extensionIdentifier: string): void { // Proxy profile provider requests through the extension host - this._profileProviders.set(id, this._terminalService.registerTerminalProfileProvider(extensionIdentifier, id, { + this._profileProviders.set(id, this._terminalProfileService.registerTerminalProfileProvider(extensionIdentifier, id, { createContributedTerminalProfile: async (options) => { return this._proxy.$createContributedProfileTerminal(id, options); } @@ -303,18 +306,6 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape proxy.onRequestLatency(() => this._onRequestLatency(proxy.instanceId)); } - public $sendProcessTitle(terminalId: number, title: string): void { - // Since title events can only come from vscode.Pseudoterminals right now, these are routed - // directly to the instance as API source events such that they will replace the initial - // `name` property provided for the Pseudoterminal. If we support showing both Api and - // Process titles at the same time we may want to pass this through as a Process source - // event. - const instance = this._terminalService.getInstanceFromId(terminalId); - if (instance) { - instance.refreshTabLabels(title, TitleEventSource.Api); - } - } - public $sendProcessData(terminalId: number, data: string): void { this._terminalProcessProxies.get(terminalId)?.emitData(data); } @@ -323,24 +314,14 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._terminalProcessProxies.get(terminalId)?.emitReady(pid, cwd); } - public $sendProcessExit(terminalId: number, exitCode: number | undefined): void { - this._terminalProcessProxies.get(terminalId)?.emitExit(exitCode); - } - - public $sendOverrideDimensions(terminalId: number, dimensions: ITerminalDimensions | undefined): void { - this._terminalProcessProxies.get(terminalId)?.emitOverrideDimensions(dimensions); - } - - public $sendProcessInitialCwd(terminalId: number, initialCwd: string): void { - this._terminalProcessProxies.get(terminalId)?.emitInitialCwd(initialCwd); - } - - public $sendProcessCwd(terminalId: number, cwd: string): void { - this._terminalProcessProxies.get(terminalId)?.emitCwd(cwd); - } - - public $sendResolvedLaunchConfig(terminalId: number, shellLaunchConfig: IShellLaunchConfig): void { - this._getTerminalProcess(terminalId)?.emitResolvedShellLaunchConfig(shellLaunchConfig); + public $sendProcessProperty(terminalId: number, property: IProcessProperty<any>): void { + if (property.type === ProcessPropertyType.Title) { + const instance = this._terminalService.getInstanceFromId(terminalId); + if (instance) { + instance.refreshTabLabels(property.value, TitleEventSource.Api); + } + } + this._terminalProcessProxies.get(terminalId)?.emitProcessProperty(property); } private async _onRequestLatency(terminalId: number): Promise<void> { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 75cfaf00eb2..6779800a4b7 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -39,7 +39,7 @@ import { IRemoteConnectionData, RemoteAuthorityResolverErrorCode, ResolverResult import { ProvidedPortAttributes, TunnelCreationOptions, TunnelOptions, TunnelProviderFeatures } from 'vs/platform/remote/common/tunnel'; import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; import { ITelemetryInfo } from 'vs/platform/telemetry/common/telemetry'; -import { ICreateContributedTerminalProfileOptions, IShellLaunchConfig, IShellLaunchConfigDto, ITerminalDimensions, ITerminalEnvironment, ITerminalLaunchError, ITerminalProfile, TerminalLocation } from 'vs/platform/terminal/common/terminal'; +import { ICreateContributedTerminalProfileOptions, IProcessProperty, IShellLaunchConfigDto, ITerminalEnvironment, ITerminalLaunchError, ITerminalProfile, TerminalLocation } from 'vs/platform/terminal/common/terminal'; import { ThemeColor, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync'; import { WorkspaceTrustRequestOptions } from 'vs/platform/workspace/common/workspaceTrust'; @@ -154,7 +154,7 @@ export interface CommentProviderFeatures { export type CommentThreadChanges = Partial<{ range: IRange, label: string, - contextValue: string, + contextValue: string | null, comments: modes.Comment[], collapseState: modes.CommentThreadCollapsibleState; canReply: boolean; @@ -505,14 +505,10 @@ export interface MainThreadTerminalServiceShape extends IDisposable { $setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: ISerializableEnvironmentVariableCollection | undefined): void; // Process - $sendProcessTitle(terminalId: number, title: string): void; $sendProcessData(terminalId: number, data: string): void; $sendProcessReady(terminalId: number, pid: number, cwd: string): void; + $sendProcessProperty(terminalId: number, property: IProcessProperty<any>): void; $sendProcessExit(terminalId: number, exitCode: number | undefined): void; - $sendProcessInitialCwd(terminalId: number, cwd: string): void; - $sendProcessCwd(terminalId: number, initialCwd: string): void; - $sendOverrideDimensions(terminalId: number, dimensions: ITerminalDimensions | undefined): void; - $sendResolvedLaunchConfig(terminalId: number, shellLaunchConfig: IShellLaunchConfig): void; } export interface TransferQuickPickItems extends quickInput.IQuickPickItem { diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index f15578fb0ae..ab15113e655 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -425,7 +425,11 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo formattedModifications.label = this.label; } if (modified('contextValue')) { - formattedModifications.contextValue = this.contextValue; + /* + * null -> cleared contextValue + * undefined -> no change + */ + formattedModifications.contextValue = this.contextValue ?? null; } if (modified('comments')) { formattedModifications.comments = diff --git a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts b/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts index bf2bbb30e97..aa0d9c70151 100644 --- a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts @@ -42,8 +42,8 @@ export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextD this._disposables.add(extHostDocuments.onDidChangeDocument(e => { const cellIdx = this._cellUris.get(e.document.uri); if (cellIdx !== undefined) { - this._cellLengths.changeValue(cellIdx, this._cells[cellIdx].document.getText().length + 1); - this._cellLines.changeValue(cellIdx, this._cells[cellIdx].document.lineCount); + this._cellLengths.setValue(cellIdx, this._cells[cellIdx].document.getText().length + 1); + this._cellLines.setValue(cellIdx, this._cells[cellIdx].document.lineCount); this._versionId += 1; this._onDidChange.fire(undefined); } diff --git a/src/vs/workbench/api/common/extHostTask.ts b/src/vs/workbench/api/common/extHostTask.ts index 13339a21ba2..baceec91f5c 100644 --- a/src/vs/workbench/api/common/extHostTask.ts +++ b/src/vs/workbench/api/common/extHostTask.ts @@ -492,10 +492,6 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask public async $onDidStartTask(execution: tasks.TaskExecutionDTO, terminalId: number, resolvedDefinition: tasks.TaskDefinitionDTO): Promise<void> { const customExecution: types.CustomExecution | undefined = this._providedCustomExecutions2.get(execution.id); if (customExecution) { - if (this._activeCustomExecutions2.get(execution.id) !== undefined) { - throw new Error('We should not be trying to start the same custom task executions twice.'); - } - // Clone the custom execution to keep the original untouched. This is important for multiple runs of the same task. this._activeCustomExecutions2.set(execution.id, customExecution); this._terminalService.attachPtyToTerminal(terminalId, await customExecution.callback(resolvedDefinition)); @@ -625,6 +621,8 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask const taskId = await this._proxy.$createTaskId(taskDTO); if (!isProvided && !this._providedCustomExecutions2.has(taskId)) { this._notProvidedCustomExecutions.add(taskId); + // Also add to active executions when not coming from a provider to prevent timing issue. + this._activeCustomExecutions2.set(taskId, <types.CustomExecution>task.execution); } this._providedCustomExecutions2.set(taskId, <types.CustomExecution>task.execution); } @@ -642,13 +640,20 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask if (result) { return result; } - // eslint-disable-next-line no-async-promise-executor - const createdResult: Promise<TaskExecutionImpl> = new Promise(async (resolve, reject) => { - const taskToCreate = task ? task : await TaskDTO.to(execution.task, this._workspaceProvider, this._providedCustomExecutions2); - if (!taskToCreate) { - reject('Unexpected: Task does not exist.'); + const createdResult: Promise<TaskExecutionImpl> = new Promise((resolve, reject) => { + function resolvePromiseWithCreatedTask(that: ExtHostTaskBase, execution: tasks.TaskExecutionDTO, taskToCreate: vscode.Task | types.Task | undefined) { + if (!taskToCreate) { + reject('Unexpected: Task does not exist.'); + } else { + resolve(new TaskExecutionImpl(that, execution.id, taskToCreate)); + } + } + + if (task) { + resolvePromiseWithCreatedTask(this, execution, task); } else { - resolve(new TaskExecutionImpl(this, execution.id, taskToCreate)); + TaskDTO.to(execution.task, this._workspaceProvider, this._providedCustomExecutions2) + .then(task => resolvePromiseWithCreatedTask(this, execution, task)); } }); diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 0309ac1ff3b..af9e4acda97 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -18,7 +18,7 @@ import { serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/ter import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { generateUuid } from 'vs/base/common/uuid'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { ICreateContributedTerminalProfileOptions, IProcessReadyEvent, IShellLaunchConfigDto, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalProfile, TerminalIcon, TerminalLocation, IProcessProperty, TerminalShellType, IShellLaunchConfig, ProcessPropertyType, ProcessCapability, IProcessPropertyMap } from 'vs/platform/terminal/common/terminal'; +import { ICreateContributedTerminalProfileOptions, IProcessReadyEvent, IShellLaunchConfigDto, ITerminalChildProcess, ITerminalLaunchError, ITerminalProfile, TerminalIcon, TerminalLocation, IProcessProperty, ProcessPropertyType, ProcessCapability, IProcessPropertyMap } from 'vs/platform/terminal/common/terminal'; import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { withNullAsUndefined } from 'vs/base/common/types'; @@ -249,30 +249,21 @@ export class ExtHostPseudoterminal implements ITerminalChildProcess { get capabilities(): ProcessCapability[] { return this._capabilities; } private readonly _onProcessData = new Emitter<string>(); public readonly onProcessData: Event<string> = this._onProcessData.event; - private readonly _onProcessExit = new Emitter<number | undefined>(); - public readonly onProcessExit: Event<number | undefined> = this._onProcessExit.event; private readonly _onProcessReady = new Emitter<IProcessReadyEvent>(); public get onProcessReady(): Event<IProcessReadyEvent> { return this._onProcessReady.event; } - private readonly _onProcessTitleChanged = new Emitter<string>(); - public readonly onProcessTitleChanged: Event<string> = this._onProcessTitleChanged.event; - private readonly _onProcessOverrideDimensions = new Emitter<ITerminalDimensionsOverride | undefined>(); - public get onProcessOverrideDimensions(): Event<ITerminalDimensionsOverride | undefined> { return this._onProcessOverrideDimensions.event; } - private readonly _onProcessShellTypeChanged = new Emitter<TerminalShellType>(); - public readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; private readonly _onDidChangeProperty = new Emitter<IProcessProperty<any>>(); public readonly onDidChangeProperty = this._onDidChangeProperty.event; - + private readonly _onProcessExit = new Emitter<number | undefined>(); + public readonly onProcessExit: Event<number | undefined> = this._onProcessExit.event; constructor(private readonly _pty: vscode.Pseudoterminal) { } - onProcessResolvedShellLaunchConfig?: Event<IShellLaunchConfig> | undefined; - onDidChangeHasChildProcesses?: Event<boolean> | undefined; refreshProperty<T extends ProcessPropertyType>(property: ProcessPropertyType): Promise<IProcessPropertyMap[T]> { - return Promise.resolve(''); + throw new Error(`refreshProperty is not suppported in extension owned terminals. property: ${property}`); } - async updateProperty<T extends ProcessPropertyType>(property: ProcessPropertyType, value: IProcessPropertyMap[T]): Promise<void> { - Promise.resolve(''); + updateProperty<T extends ProcessPropertyType>(property: ProcessPropertyType, value: IProcessPropertyMap[T]): Promise<void> { + throw new Error(`updateProperty is not suppported in extension owned terminals. property: ${property}, value: ${value}`); } async start(): Promise<undefined> { @@ -329,10 +320,16 @@ export class ExtHostPseudoterminal implements ITerminalChildProcess { }); } if (this._pty.onDidOverrideDimensions) { - this._pty.onDidOverrideDimensions(e => this._onProcessOverrideDimensions.fire(e ? { cols: e.columns, rows: e.rows } : e)); + this._pty.onDidOverrideDimensions(e => { + if (e) { + this._onDidChangeProperty.fire({ type: ProcessPropertyType.OverrideDimensions, value: { cols: e.columns, rows: e.rows } }); + } + }); } if (this._pty.onDidChangeName) { - this._pty.onDidChangeName(title => this._onProcessTitleChanged.fire(title)); + this._pty.onDidChangeName(title => { + this._onDidChangeProperty.fire({ type: ProcessPropertyType.Title, value: title }); + }); } this._pty.open(initialDimensions ? initialDimensions : undefined); @@ -589,17 +586,12 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I protected _setupExtHostProcessListeners(id: number, p: ITerminalChildProcess): IDisposable { const disposables = new DisposableStore(); - disposables.add(p.onProcessReady((e: { pid: number, cwd: string }) => this._proxy.$sendProcessReady(id, e.pid, e.cwd))); - disposables.add(p.onProcessTitleChanged(title => this._proxy.$sendProcessTitle(id, title))); + disposables.add(p.onDidChangeProperty(property => this._proxy.$sendProcessProperty(id, property))); // Buffer data events to reduce the amount of messages going to the renderer this._bufferer.startBuffering(id, p.onProcessData); disposables.add(p.onProcessExit(exitCode => this._onProcessExit(id, exitCode))); - - if (p.onProcessOverrideDimensions) { - disposables.add(p.onProcessOverrideDimensions(e => this._proxy.$sendOverrideDimensions(id, e))); - } this._terminalProcesses.set(id, p); const awaitingStart = this._extensionTerminalAwaitingStart[id]; @@ -642,11 +634,11 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } public $acceptProcessRequestInitialCwd(id: number): void { - this._terminalProcesses.get(id)?.getInitialCwd().then(initialCwd => this._proxy.$sendProcessInitialCwd(id, initialCwd)); + this._terminalProcesses.get(id)?.getInitialCwd().then(initialCwd => this._proxy.$sendProcessProperty(id, { type: ProcessPropertyType.InitialCwd, value: initialCwd })); } public $acceptProcessRequestCwd(id: number): void { - this._terminalProcesses.get(id)?.getCwd().then(cwd => this._proxy.$sendProcessCwd(id, cwd)); + this._terminalProcesses.get(id)?.getCwd().then(cwd => this._proxy.$sendProcessProperty(id, { type: ProcessPropertyType.Cwd, value: cwd })); } public $acceptProcessRequestLatency(id: number): number { @@ -777,7 +769,6 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I processDiposable.dispose(); delete this._terminalProcessDisposables[id]; } - // Send exit event to main side this._proxy.$sendProcessExit(id, exitCode); } diff --git a/src/vs/workbench/browser/actions/developerActions.ts b/src/vs/workbench/browser/actions/developerActions.ts index 0d032154fe0..6361a12975b 100644 --- a/src/vs/workbench/browser/actions/developerActions.ts +++ b/src/vs/workbench/browser/actions/developerActions.ts @@ -19,7 +19,7 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { timeout } from 'vs/base/common/async'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { Registry } from 'vs/platform/registry/common/platform'; -import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { registerAction2, Action2, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { clamp } from 'vs/base/common/numbers'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -217,11 +217,37 @@ class ToggleScreencastModeAction extends Action2 { length = 0; } + const format = configurationService.getValue<'keys' | 'command' | 'commandWithGroup' | 'commandAndKeys' | 'commandWithGroupAndKeys'>('screencastMode.keyboardShortcutsFormat'); const keybinding = keybindingService.resolveKeyboardEvent(event); - const label = keybinding.getLabel(); - const key = $('span.key', {}, label || ''); + const command = shortcut?.commandId ? MenuRegistry.getCommand(shortcut.commandId) : null; + + let titleLabel = ''; + let keyLabel = keybinding.getLabel(); + + if (command) { + titleLabel = typeof command.title === 'string' ? command.title : command.title.value; + + if ((format === 'commandWithGroup' || format === 'commandWithGroupAndKeys') && command.category) { + titleLabel = `${typeof command.category === 'string' ? command.category : command.category.value}: ${titleLabel} `; + } + + if (shortcut?.commandId) { + const fullKeyLabel = keybindingService.lookupKeybinding(shortcut.commandId); + if (fullKeyLabel) { + keyLabel = fullKeyLabel.getLabel(); + } + } + } + + if (format !== 'keys' && titleLabel) { + append(keyboardMarker, $('span.title', {}, `${titleLabel} `)); + } + + if (format === 'keys' || format === 'commandAndKeys' || format === 'commandWithGroupAndKeys') { + append(keyboardMarker, $('span.key', {}, keyLabel || '')); + } + length++; - append(keyboardMarker, key); } const promise = timeout(keyboardMarkerTimeout); @@ -318,6 +344,18 @@ configurationRegistry.registerConfiguration({ maximum: 100, description: localize('screencastMode.fontSize', "Controls the font size (in pixels) of the screencast mode keyboard.") }, + 'screencastMode.keyboardShortcutsFormat': { + enum: ['keys', 'command', 'commandWithGroup', 'commandAndKeys', 'commandWithGroupAndKeys'], + enumDescriptions: [ + localize('keyboardShortcutsFormat.keys', "Keys."), + localize('keyboardShortcutsFormat.command', "Command title."), + localize('keyboardShortcutsFormat.commandWithGroup', "Command title prefixed by its group."), + localize('keyboardShortcutsFormat.commandAndKeys', "Command title and keys."), + localize('keyboardShortcutsFormat.commandWithGroupAndKeys', "Command title and keys, with the command prefixed by its group.") + ], + description: localize('screencastMode.keyboardShortcutsFormat', "Controls what is displayed in the keyboard overlay when showing only shortcuts."), + default: 'commandAndKeys' + }, 'screencastMode.onlyKeyboardShortcuts': { type: 'boolean', description: localize('screencastMode.onlyKeyboardShortcuts', "Only show keyboard shortcuts in screencast mode."), diff --git a/src/vs/workbench/browser/actions/media/actions.css b/src/vs/workbench/browser/actions/media/actions.css index 54aeeae495a..534056c0753 100644 --- a/src/vs/workbench/browser/actions/media/actions.css +++ b/src/vs/workbench/browser/actions/media/actions.css @@ -46,3 +46,7 @@ border-radius: 5px; background-color: rgba(255, 255, 255, 0.05); } + +.monaco-workbench .screencast-keyboard > .title { + font-weight: 600; +} diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index e52386560c6..d5623298ef1 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -1136,6 +1136,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.state.zenMode.wasAuxiliaryBarPartVisible = this.isVisible(Parts.AUXILIARYBAR_PART); this.setPanelHidden(true, true); + this.setAuxiliaryBarHidden(true, true); this.setSideBarHidden(true, true); if (config.hideActivityBar) { @@ -1178,6 +1179,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.setPanelHidden(false, true); } + if (this.state.zenMode.wasAuxiliaryBarPartVisible) { + this.setAuxiliaryBarHidden(false, true); + } + if (this.state.zenMode.wasSideBarVisible) { this.setSideBarHidden(false, true); } @@ -1684,7 +1689,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } private setAuxiliaryBarHidden(hidden: boolean, skipLayout?: boolean): void { - if (!this.configurationService.getValue(Settings.AUXILIARYBAR_ENABLED)) { + if (!this.configurationService || !this.configurationService.getValue(Settings.AUXILIARYBAR_ENABLED)) { return; } diff --git a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts index 137e6bd4023..aa3b4855816 100644 --- a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts +++ b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts @@ -95,7 +95,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution return; // no auto save for readonly or untitled editors } - // Determine if we need to save all. In case of a window focus change we also save if + // Determine if we need to save all. In case of a window focus change we also save if // auto save mode is configured to be ON_FOCUS_CHANGE (editor focus change) const mode = this.filesConfigurationService.getAutoSaveMode(); if ( @@ -115,7 +115,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution private onAutoSaveConfigurationChange(config: IAutoSaveConfiguration, fromEvent: boolean): void { // Update auto save after delay config - this.autoSaveAfterDelay = (typeof config.autoSaveDelay === 'number') && config.autoSaveDelay > 0 ? config.autoSaveDelay : undefined; + this.autoSaveAfterDelay = (typeof config.autoSaveDelay === 'number') && config.autoSaveDelay >= 0 ? config.autoSaveDelay : undefined; // Trigger a save-all when auto save is enabled if (fromEvent) { diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 53179f98fb8..b446ee591ad 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -1059,26 +1059,31 @@ export class EditorGroupView extends Themable implements IEditorGroupView { let openEditorPromise: Promise<IEditorPane | undefined>; if (context.active) { openEditorPromise = (async () => { - const result = await this.editorPane.openEditor(editor, options, { newInGroup: context.isNew }); + const { pane, changed, cancelled, error } = await this.editorPane.openEditor(editor, options, { newInGroup: context.isNew }); + + // Return early if the operation was cancelled by another operation + if (cancelled) { + return undefined; + } // Editor change event - if (result.editorChanged) { + if (changed) { this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_ACTIVE, editor }); } // Handle errors but do not bubble them up - if (result.error) { - await this.doHandleOpenEditorError(result.error, editor, options); + if (error) { + await this.doHandleOpenEditorError(error, editor, options); } // Without an editor pane, recover by closing the active editor // (if the input is still the active one) - if (!result.editorPane && this.activeEditor === editor) { + if (!pane && this.activeEditor === editor) { const focusNext = !options || !options.preserveFocus; this.doCloseEditor(editor, focusNext, { fromError: true }); } - return result.editorPane; + return pane; })(); } else { openEditorPromise = Promise.resolve(undefined); // inactive: return undefined as result to signal this diff --git a/src/vs/workbench/browser/parts/editor/editorPanes.ts b/src/vs/workbench/browser/parts/editor/editorPanes.ts index ccb86f9477d..13e9fbc0e8d 100644 --- a/src/vs/workbench/browser/parts/editor/editorPanes.ts +++ b/src/vs/workbench/browser/parts/editor/editorPanes.ts @@ -12,7 +12,7 @@ import { IEditorPaneRegistry, IEditorPaneDescriptor } from 'vs/workbench/browser import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorProgressService, LongRunningOperation } from 'vs/platform/progress/common/progress'; +import { IEditorProgressService, IOperation, LongRunningOperation } from 'vs/platform/progress/common/progress'; import { IEditorGroupView, DEFAULT_EDITOR_MIN_DIMENSIONS, DEFAULT_EDITOR_MAX_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; import { Emitter } from 'vs/base/common/event'; import { assertIsDefined } from 'vs/base/common/types'; @@ -32,12 +32,12 @@ export interface IOpenEditorResult { * open the editor and in cases where no placeholder is being * used. */ - readonly editorPane?: EditorPane; + readonly pane?: EditorPane; /** * Whether the editor changed as a result of opening. */ - readonly editorChanged?: boolean; + readonly changed?: boolean; /** * This property is set when an editor fails to restore and @@ -45,6 +45,14 @@ export interface IOpenEditorResult { * to still present the error to the user in that case. */ readonly error?: Error; + + /** + * This property indicates whether the open editor operation was + * cancelled or not. The operation may have been cancelled + * in case another editor open operation was triggered right + * after cancelling this one out. + */ + readonly cancelled?: boolean; } export class EditorPanes extends Disposable { @@ -136,11 +144,32 @@ export class EditorPanes extends Disposable { private async doOpenEditor(descriptor: IEditorPaneDescriptor, editor: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext = Object.create(null)): Promise<IOpenEditorResult> { // Editor pane - const editorPane = this.doShowEditorPane(descriptor); + const pane = this.doShowEditorPane(descriptor); + + // Show progress while setting input after a certain timeout. + // If the workbench is opening be more relaxed about progress + // showing by increasing the delay a little bit to reduce flicker. + const operation = this.editorOperation.start(this.layoutService.isRestored() ? 800 : 3200); // Apply input to pane - const editorChanged = await this.doSetInput(editorPane, editor, options, context); - return { editorPane, editorChanged }; + let changed: boolean; + let cancelled: boolean; + try { + changed = await this.doSetInput(pane, operation, editor, options, context); + cancelled = !operation.isCurrent(); + } finally { + operation.stop(); + } + + // Focus unless cancelled + if (!cancelled) { + const focus = !options || !options.preserveFocus; + if (focus) { + pane.focus(); + } + } + + return { pane, changed, cancelled }; } private getEditorPaneDescriptor(editor: EditorInput): IEditorPaneDescriptor { @@ -234,47 +263,22 @@ export class EditorPanes extends Disposable { this._onDidChangeSizeConstraints.fire(undefined); } - private async doSetInput(editorPane: EditorPane, editor: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext): Promise<boolean> { + private async doSetInput(editorPane: EditorPane, operation: IOperation, editor: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext): Promise<boolean> { + const forceReload = options?.forceReload; + const inputMatches = editorPane.input?.matches(editor); // If the input did not change, return early and only apply the options // unless the options instruct us to force open it even if it is the same - const forceReload = options?.forceReload; - const inputMatches = editorPane.input && editorPane.input.matches(editor); if (inputMatches && !forceReload) { - - // Forward options editorPane.setOptions(options); - - // Still focus as needed - const focus = !options || !options.preserveFocus; - if (focus) { - editorPane.focus(); - } - - return false; } - // Show progress while setting input after a certain timeout. If the workbench is opening - // be more relaxed about progress showing by increasing the delay a little bit to reduce flicker. - const operation = this.editorOperation.start(this.layoutService.isRestored() ? 800 : 3200); - - // Call into editor pane - const editorWillChange = !inputMatches; - try { + // Otherwise set the input to the editor pane + else { await editorPane.setInput(editor, options, context, operation.token); - - // Focus (unless prevented or another operation is running) - if (operation.isCurrent()) { - const focus = !options || !options.preserveFocus; - if (focus) { - editorPane.focus(); - } - } - - return editorWillChange; - } finally { - operation.stop(); } + + return !inputMatches; } private doHideActiveEditorPane(): void { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts index 2c1d47f2a93..4f3ee08b85b 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts @@ -157,7 +157,9 @@ export class NotificationsCenter extends Themable implements INotificationsCente notificationsToolBar.push(hideAllAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(hideAllAction) }); // Notifications List - this.notificationsList = this.instantiationService.createInstance(NotificationsList, this.notificationsCenterContainer, {}); + this.notificationsList = this.instantiationService.createInstance(NotificationsList, this.notificationsCenterContainer, { + widgetAriaLabel: localize('notificationsCenterWidgetAriaLabel', "Notifications Center") + }); this.container.appendChild(this.notificationsCenterContainer); } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsList.ts b/src/vs/workbench/browser/parts/notifications/notificationsList.ts index ec27091c6af..cc95aea004a 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsList.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsList.ts @@ -20,7 +20,12 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; import { Codicon } from 'vs/base/common/codicons'; +export interface INotificationsListOptions extends IListOptions<INotificationViewItem> { + widgetAriaLabel?: string; +} + export class NotificationsList extends Themable { + private listContainer: HTMLElement | undefined; private list: WorkbenchList<INotificationViewItem> | undefined; private listDelegate: NotificationsListDelegate | undefined; @@ -29,7 +34,7 @@ export class NotificationsList extends Themable { constructor( private readonly container: HTMLElement, - private readonly options: IListOptions<INotificationViewItem>, + private readonly options: INotificationsListOptions, @IInstantiationService private readonly instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @IContextMenuService private readonly contextMenuService: IContextMenuService @@ -75,6 +80,7 @@ export class NotificationsList extends Themable { // List const listDelegate = this.listDelegate = new NotificationsListDelegate(this.listContainer); + const options = this.options; const list = this.list = <WorkbenchList<INotificationViewItem>>this._register(this.instantiationService.createInstance( WorkbenchList, 'NotificationsList', @@ -82,7 +88,7 @@ export class NotificationsList extends Themable { listDelegate, [renderer], { - ...this.options, + ...options, setRowLineHeight: false, horizontalScrolling: false, overrideStyles: { @@ -97,7 +103,7 @@ export class NotificationsList extends Themable { return localize('notificationWithSourceAriaLabel', "{0}, source: {1}, notification", element.message.raw, element.source); }, getWidgetAriaLabel(): string { - return localize('notificationsList', "Notifications List"); + return options.widgetAriaLabel ?? localize('notificationsList', "Notifications List"); }, getRole(): string { return 'dialog'; // https://github.com/microsoft/vscode/issues/82728 diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index f8482094e2c..6716e45b671 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/notificationsToasts'; +import { localize } from 'vs/nls'; import { INotificationsModel, NotificationChangeType, INotificationChangeEvent, INotificationViewItem, NotificationViewItemContentChangeKind } from 'vs/workbench/common/notifications'; import { IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { isAncestor, addDisposableListener, EventType, Dimension, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; @@ -183,7 +184,14 @@ export class NotificationsToasts extends Themable implements INotificationsToast // Create toast with item and show const notificationList = this.instantiationService.createInstance(NotificationsList, notificationToast, { - verticalScrollMode: ScrollbarVisibility.Hidden + verticalScrollMode: ScrollbarVisibility.Hidden, + widgetAriaLabel: (() => { + if (!item.source) { + return localize('notificationAriaLabel', "{0}, notification", item.message.raw); + } + + return localize('notificationWithSourceAriaLabel', "{0}, source: {1}, notification", item.message.raw, item.source); + })() }); itemDisposables.add(notificationList); diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 2e29a8fca84..d6d37e947c8 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -269,7 +269,7 @@ export abstract class BasePanelPart extends CompositePart<PaneComposite> impleme if (activeContainers.length) { if (this.getActivePaneComposite()?.getId() === panelId) { - const defaultPanelId = this.viewDescriptorService.getDefaultViewContainer(this.viewContainerLocation)!.id; + const defaultPanelId = this.viewDescriptorService.getDefaultViewContainer(this.viewContainerLocation)?.id; const containerToOpen = activeContainers.filter(c => c.id === defaultPanelId)[0] || activeContainers[0]; await this.openPaneComposite(containerToOpen.id); } diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index a7609bc98e5..e84fe69e9a1 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -922,10 +922,8 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS return { markdown: (token: CancellationToken): Promise<IMarkdownString | string | undefined> => { - // eslint-disable-next-line no-async-promise-executor - return new Promise<IMarkdownString | string | undefined>(async (resolve) => { - await node.resolve(token); - resolve(node.tooltip); + return new Promise<IMarkdownString | string | undefined>((resolve) => { + node.resolve(token).then(() => resolve(node.tooltip)); }); }, markdownNotSupportedFallback: resource ? undefined : label ?? '' // Passing undefined as the fallback for a resource falls back to the old native hover diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 77453c49383..9047c2bb836 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -8,7 +8,7 @@ import { domContentLoaded, detectFullscreen, getCookieValue, WebFileSystemAccess import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILogService, ConsoleLogger, MultiplexLogService, getLogLevel } from 'vs/platform/log/common/log'; import { ConsoleLogInAutomationLogger } from 'vs/platform/log/browser/log'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { Workbench } from 'vs/workbench/browser/workbench'; import { RemoteFileSystemProvider } from 'vs/workbench/services/remote/common/remoteAgentFileSystemChannel'; @@ -19,7 +19,7 @@ import { RemoteAgentService } from 'vs/workbench/services/remote/browser/remoteA import { RemoteAuthorityResolverService } from 'vs/platform/remote/browser/remoteAuthorityResolverService'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { IFileService, IFileSystemProvider } from 'vs/platform/files/common/files'; +import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; import { Schemas } from 'vs/base/common/network'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -43,7 +43,7 @@ import { getSingleFolderWorkspaceIdentifier, getWorkspaceIdentifier } from 'vs/w import { coalesce } from 'vs/base/common/arrays'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IIndexedDBFileSystemProvider, IndexedDB, INDEXEDDB_LOGS_OBJECT_STORE, INDEXEDDB_USERDATA_OBJECT_STORE } from 'vs/platform/files/browser/indexedDBFileSystemProvider'; +import { IndexedDBFileSystemProvider } from 'vs/platform/files/browser/indexedDBFileSystemProvider'; import { BrowserRequestService } from 'vs/workbench/services/request/browser/requestService'; import { IRequestService } from 'vs/platform/request/common/request'; import { IUserDataInitializationService, UserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; @@ -66,9 +66,12 @@ import { HTMLFileSystemProvider } from 'vs/platform/files/browser/htmlFileSystem import { IOpenerService } from 'vs/platform/opener/common/opener'; import { safeStringify } from 'vs/base/common/objects'; import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials'; +import { IndexedDB } from 'vs/base/browser/indexedDB'; class BrowserMain extends Disposable { + private readonly onWillShutdownDisposables = new DisposableStore(); + constructor( private readonly domElement: HTMLElement, private readonly configuration: IWorkbenchConstructionOptions @@ -140,7 +143,7 @@ class BrowserMain extends Disposable { event.veto(true, 'veto.pendingStorageUpdate'); // prevent data loss from pending storage update } })); - this._register(workbench.onWillShutdown(() => storageService.close())); + this._register(workbench.onWillShutdown(() => this.onWillShutdownDisposables.clear())); this._register(workbench.onDidShutdown(() => this.dispose())); } @@ -251,59 +254,43 @@ class BrowserMain extends Disposable { } private async registerFileSystemProviders(environmentService: IWorkbenchEnvironmentService, fileService: IFileService, remoteAgentService: IRemoteAgentService, logService: BufferLogService, logsPath: URI): Promise<void> { - const indexedDB = new IndexedDB(); + let indexedDB: IndexedDB | undefined; + const userDataStore = 'vscode-userdata-store', logsStore = 'vscode-logs-store'; + try { + indexedDB = await IndexedDB.create('vscode-web-db', 2, [userDataStore, logsStore]); + this.onWillShutdownDisposables.add(toDisposable(() => indexedDB?.close())); + } catch (error) { + logService.error('Error while creating IndexedDB'); + logService.error(error); + } // Logger - (async () => { - let indexedDBLogProvider: IFileSystemProvider | null = null; - try { - indexedDBLogProvider = await indexedDB.createFileSystemProvider(logsPath.scheme, INDEXEDDB_LOGS_OBJECT_STORE, false); - } catch (error) { - onUnexpectedError(error); - } - - if (indexedDBLogProvider) { - fileService.registerProvider(logsPath.scheme, indexedDBLogProvider); - } else { - fileService.registerProvider(logsPath.scheme, new InMemoryFileSystemProvider()); - } - - logService.logger = new MultiplexLogService(coalesce([ - new ConsoleLogger(logService.getLevel()), - new FileLogger('window', environmentService.logFile, logService.getLevel(), false, fileService), - // Extension development test CLI: forward everything to test runner - environmentService.isExtensionDevelopment && !!environmentService.extensionTestsLocationURI ? new ConsoleLogInAutomationLogger(logService.getLevel()) : undefined - ])); - })(); - - // Remote file system - const connection = remoteAgentService.getConnection(); - if (connection) { - const remoteFileSystemProvider = this._register(new RemoteFileSystemProvider(remoteAgentService)); - fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider); + if (indexedDB) { + fileService.registerProvider(logsPath.scheme, new IndexedDBFileSystemProvider(logsPath.scheme, indexedDB, logsStore, false)); + } else { + fileService.registerProvider(logsPath.scheme, new InMemoryFileSystemProvider()); } + logService.logger = new MultiplexLogService(coalesce([ + new ConsoleLogger(logService.getLevel()), + new FileLogger('window', environmentService.logFile, logService.getLevel(), false, fileService), + // Extension development test CLI: forward everything to test runner + environmentService.isExtensionDevelopment && !!environmentService.extensionTestsLocationURI ? new ConsoleLogInAutomationLogger(logService.getLevel()) : undefined + ])); // User data - let indexedDBUserDataProvider: IIndexedDBFileSystemProvider | null = null; - try { - indexedDBUserDataProvider = await indexedDB.createFileSystemProvider(Schemas.userData, INDEXEDDB_USERDATA_OBJECT_STORE, true); - } catch (error) { - onUnexpectedError(error); - } - - let userDataProvider: IFileSystemProvider | undefined; - if (indexedDBUserDataProvider) { - userDataProvider = indexedDBUserDataProvider; - - this.registerDeveloperActions(indexedDBUserDataProvider); + let userDataProvider; + if (indexedDB) { + userDataProvider = new IndexedDBFileSystemProvider(logsPath.scheme, indexedDB, userDataStore, false); + this.registerDeveloperActions(<IndexedDBFileSystemProvider>userDataProvider); } else { logService.info('Using in-memory user data provider'); - userDataProvider = new InMemoryFileSystemProvider(); } - fileService.registerProvider(Schemas.userData, userDataProvider); + // Remote file system + this._register(RemoteFileSystemProvider.register(remoteAgentService, fileService, logService)); + // Local file access (if supported by browser) if (WebFileSystemAccess.supported(window)) { fileService.registerProvider(Schemas.file, new HTMLFileSystemProvider()); @@ -313,7 +300,7 @@ class BrowserMain extends Disposable { fileService.registerProvider(Schemas.tmp, new InMemoryFileSystemProvider()); } - private registerDeveloperActions(provider: IIndexedDBFileSystemProvider): void { + private registerDeveloperActions(provider: IndexedDBFileSystemProvider): void { registerAction2(class ResetUserDataAction extends Action2 { constructor() { super({ @@ -356,7 +343,7 @@ class BrowserMain extends Disposable { try { await storageService.initialize(); - + this.onWillShutdownDisposables.add(toDisposable(() => storageService.close())); return storageService; } catch (error) { onUnexpectedError(error); diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index 71f9b9304ec..cdfe8e4855f 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -17,7 +17,6 @@ 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 { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; @@ -32,7 +31,6 @@ export class BrowserWindow extends Disposable { @IDialogService private readonly dialogService: IDialogService, @ILabelService private readonly labelService: ILabelService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, - @ILogService private readonly logService: ILogService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService ) { super(); @@ -49,9 +47,10 @@ export class BrowserWindow extends Disposable { // Layout const viewport = isIOS && window.visualViewport ? window.visualViewport /** Visual viewport */ : window /** Layout viewport */; this._register(addDisposableListener(viewport, EventType.RESIZE, () => { - this.onWindowResize(); + this.layoutService.layout(); + + // Sometimes the keyboard appearing scrolls the whole workbench out of view, as a workaround scroll back into view #121206 if (isIOS) { - // Sometimes the keyboard appearing scrolls the whole workbench out of view, as a workaround scroll back into view #121206 window.scrollTo(0, 0); } })); @@ -76,11 +75,6 @@ export class BrowserWindow extends Disposable { }, 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 diff --git a/src/vs/workbench/buildfile.desktop.js b/src/vs/workbench/buildfile.desktop.js index 463074b2cfe..60953cc2ff1 100644 --- a/src/vs/workbench/buildfile.desktop.js +++ b/src/vs/workbench/buildfile.desktop.js @@ -14,7 +14,6 @@ exports.collectModules = function () { createModuleDescription('vs/workbench/services/search/node/searchApp'), - createModuleDescription('vs/platform/files/node/watcher/unix/watcherApp'), createModuleDescription('vs/platform/files/node/watcher/nsfw/watcherApp'), createModuleDescription('vs/platform/files/node/watcher/parcel/watcherApp'), diff --git a/src/vs/workbench/common/resources.ts b/src/vs/workbench/common/resources.ts index 276d582bb26..b5eea2b652d 100644 --- a/src/vs/workbench/common/resources.ts +++ b/src/vs/workbench/common/resources.ts @@ -7,17 +7,17 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { deepClone, equals } from 'vs/base/common/objects'; import { Emitter } from 'vs/base/common/event'; -import { basename, dirname, extname, relativePath } from 'vs/base/common/resources'; +import { basename, dirname, extname, relativePath, isEqual } from 'vs/base/common/resources'; import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IFileService } from 'vs/platform/files/common/files'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, Disposable } from 'vs/base/common/lifecycle'; import { ParsedExpression, IExpression, parse } from 'vs/base/common/glob'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; -import { withNullAsUndefined } from 'vs/base/common/types'; +import { IModelService } from 'vs/editor/common/services/modelService'; -export class ResourceContextKey extends Disposable implements IContextKey<URI> { +export class ResourceContextKey implements IContextKey<URI> { // NOTE: DO NOT CHANGE THE DEFAULT VALUE TO ANYTHING BUT // UNDEFINED! IT IS IMPORTANT THAT DEFAULTS ARE INHERITED @@ -33,6 +33,8 @@ export class ResourceContextKey extends Disposable implements IContextKey<URI> { static readonly HasResource = new RawContextKey<boolean>('resourceSet', undefined, { type: 'boolean', description: localize('resourceSet', "Whether a resource is present or not") }); static readonly IsFileSystemResource = new RawContextKey<boolean>('isFileSystemResource', undefined, { type: 'boolean', description: localize('isFileSystemResource', "Whether the resource is backed by a file system provider") }); + private readonly _disposables = new DisposableStore(); + private readonly _resourceKey: IContextKey<URI | null>; private readonly _schemeKey: IContextKey<string | null>; private readonly _filenameKey: IContextKey<string | null>; @@ -46,10 +48,9 @@ export class ResourceContextKey extends Disposable implements IContextKey<URI> { constructor( @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IFileService private readonly _fileService: IFileService, - @IModeService private readonly _modeService: IModeService + @IModeService private readonly _modeService: IModeService, + @IModelService private readonly _modelService: IModelService, ) { - super(); - this._schemeKey = ResourceContextKey.Scheme.bindTo(this._contextKeyService); this._filenameKey = ResourceContextKey.Filename.bindTo(this._contextKeyService); this._dirnameKey = ResourceContextKey.Dirname.bindTo(this._contextKeyService); @@ -60,31 +61,53 @@ export class ResourceContextKey extends Disposable implements IContextKey<URI> { this._hasResource = ResourceContextKey.HasResource.bindTo(this._contextKeyService); this._isFileSystemResource = ResourceContextKey.IsFileSystemResource.bindTo(this._contextKeyService); - this._register(_fileService.onDidChangeFileSystemProviderRegistrations(() => { - const resource = this._resourceKey.get(); + this._disposables.add(_fileService.onDidChangeFileSystemProviderRegistrations(() => { + const resource = this.get(); this._isFileSystemResource.set(Boolean(resource && _fileService.hasProvider(resource))); })); - this._register(_modeService.onDidEncounterLanguage(() => { - const value = this._resourceKey.get(); - this._langIdKey.set(value ? this._modeService.getModeIdByFilepathOrFirstLine(value) : null); + this._disposables.add(_modeService.onDidEncounterLanguage(this._setLangId, this)); + this._disposables.add(_modelService.onModelAdded(model => { + if (isEqual(model.uri, this.get())) { + this._setLangId(); + } + })); + this._disposables.add(_modelService.onModelModeChanged(e => { + if (isEqual(e.model.uri, this.get())) { + this._setLangId(); + } })); } - set(value: URI | null) { - if (!ResourceContextKey._uriEquals(this._resourceKey.get(), value)) { - this._contextKeyService.bufferChangeEvents(() => { - this._resourceKey.set(value); - this._schemeKey.set(value ? value.scheme : null); - this._filenameKey.set(value ? basename(value) : null); - this._dirnameKey.set(value ? dirname(value).fsPath : null); - this._pathKey.set(value ? value.fsPath : null); - this._langIdKey.set(value ? this._modeService.getModeIdByFilepathOrFirstLine(value) : null); - this._extensionKey.set(value ? extname(value) : null); - this._hasResource.set(!!value); - this._isFileSystemResource.set(value ? this._fileService.hasProvider(value) : false); - }); + dispose(): void { + this._disposables.dispose(); + } + + private _setLangId(): void { + const value = this.get(); + if (!value) { + this._langIdKey.set(null); + return; } + const langId = this._modelService.getModel(value)?.getLanguageId() ?? this._modeService.getModeIdByFilepathOrFirstLine(value); + this._langIdKey.set(langId); + } + + set(value: URI | null) { + if (isEqual(this.get(), value ?? undefined)) { + return; + } + this._contextKeyService.bufferChangeEvents(() => { + this._resourceKey.set(value); + this._schemeKey.set(value ? value.scheme : null); + this._filenameKey.set(value ? basename(value) : null); + this._dirnameKey.set(value ? dirname(value).fsPath : null); + this._pathKey.set(value ? value.fsPath : null); + this._setLangId(); + this._extensionKey.set(value ? extname(value) : null); + this._hasResource.set(Boolean(value)); + this._isFileSystemResource.set(value ? this._fileService.hasProvider(value) : false); + }); } reset(): void { @@ -102,22 +125,7 @@ export class ResourceContextKey extends Disposable implements IContextKey<URI> { } get(): URI | undefined { - return withNullAsUndefined(this._resourceKey.get()); - } - - private static _uriEquals(a: URI | undefined | null, b: URI | undefined | null): boolean { - if (a === b) { - return true; - } - if (!a || !b) { - return false; - } - return a.scheme === b.scheme // checks for not equals (fail fast) - && a.authority === b.authority - && a.path === b.path - && a.query === b.query - && a.fragment === b.fragment - && a.toString() === b.toString(); // for equal we use the normalized toString-form + return this._resourceKey.get() ?? undefined; } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.css b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.css index aa88a067d33..287c5ec0963 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.css +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.css @@ -3,8 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +.monaco-workbench .bulk-edit-panel .highlight.insert { + background-color: var(--vscode-diffEditor-insertedTextBackground); +} + .monaco-workbench .bulk-edit-panel .highlight.remove { text-decoration: line-through; + background-color: var(--vscode-diffEditor-removedTextBackground); } .monaco-workbench .bulk-edit-panel .message { diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index 112ef725ad4..f95ce165dd3 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -8,8 +8,7 @@ import { WorkbenchAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/lis import { BulkEditElement, BulkEditDelegate, TextEditElementRenderer, FileElementRenderer, BulkEditDataSource, BulkEditIdentityProvider, FileElement, TextEditElement, BulkEditAccessibilityProvider, CategoryElementRenderer, BulkEditNaviLabelProvider, CategoryElement, BulkEditSorter } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree'; import { FuzzyScore } from 'vs/base/common/filters'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { registerThemingParticipant, IColorTheme, ICssStyleCollector, IThemeService } from 'vs/platform/theme/common/themeService'; -import { diffInserted, diffRemoved } from 'vs/platform/theme/common/colorRegistry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { localize } from 'vs/nls'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; @@ -378,15 +377,3 @@ export class BulkEditPane extends ViewPane { }); } } - -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { - - const diffInsertedColor = theme.getColor(diffInserted); - if (diffInsertedColor) { - collector.addRule(`.monaco-workbench .bulk-edit-panel .highlight.insert { background-color: ${diffInsertedColor}; }`); - } - const diffRemovedColor = theme.getColor(diffRemoved); - if (diffRemovedColor) { - collector.addRule(`.monaco-workbench .bulk-edit-panel .highlight.remove { background-color: ${diffRemovedColor}; }`); - } -}); diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts index 3e8b3c38c7b..d1b6c6c1aaa 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts @@ -31,7 +31,7 @@ const EDITOR_WORD_WRAP = new RawContextKey<boolean>('editorWordWrap', false, nls /** * State written/read by the toggle word wrap action and associated with a particular model. */ -interface IWordWrapTransientState { +export interface IWordWrapTransientState { readonly wordWrapOverride: 'on' | 'off'; } @@ -45,7 +45,7 @@ export function writeTransientState(model: ITextModel, state: IWordWrapTransient /** * Read (in memory) the word wrap state for a particular model. */ -function readTransientState(model: ITextModel, codeEditorService: ICodeEditorService): IWordWrapTransientState | null { +export function readTransientState(model: ITextModel, codeEditorService: ICodeEditorService): IWordWrapTransientState | null { return codeEditorService.getTransientModelProperty(model, transientWordWrapState); } diff --git a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts index 328c2abe852..a5387f666de 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts @@ -49,7 +49,7 @@ const DECORATION_KEY = 'breakpointwidgetdecoration'; function isCurlyBracketOpen(input: IActiveCodeEditor): boolean { const model = input.getModel(); - const prevBracket = model.findPrevBracket(input.getPosition()); + const prevBracket = model.bracketPairs.findPrevBracket(input.getPosition()); if (prevBracket && prevBracket.isOpen) { return true; } diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index 1fe960d8f5d..67e90cf1204 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -376,7 +376,11 @@ export class ConfigurationManager implements IConfigurationManager { } const names = launch ? launch.getConfigurationNames() : []; - this.getSelectedConfig = () => Promise.resolve(this.selectedName ? launch?.getConfiguration(this.selectedName) : undefined); + this.getSelectedConfig = () => { + const selected = this.selectedName ? launch?.getConfiguration(this.selectedName) : undefined; + return Promise.resolve(selected || config); + }; + let type = config?.type; if (name && names.indexOf(name) >= 0) { this.setSelectedLaunchName(name); diff --git a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts index 7f545d137a7..fb85e0a8efa 100644 --- a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts +++ b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts @@ -200,8 +200,7 @@ export class DebugTaskRunner { return taskPromise.then(withUndefinedAsNull); }); - // eslint-disable-next-line no-async-promise-executor - return new Promise(async (c, e) => { + return new Promise((c, e) => { const waitForInput = new Promise<void>(resolve => once(e => (e.kind === TaskEventKind.AcquiredInput) && e.taskId === task._id, this.taskService.onDidStateChange)(() => { resolve(); })); @@ -211,17 +210,18 @@ export class DebugTaskRunner { c(result); }, error => e(error)); - await waitForInput; - const waitTime = task.configurationProperties.isBackground ? 5000 : 10000; + waitForInput.then(() => { + const waitTime = task.configurationProperties.isBackground ? 5000 : 10000; - setTimeout(() => { - if (!taskStarted) { - const errorMessage = typeof taskId === 'string' - ? nls.localize('taskNotTrackedWithTaskId', "The task '{0}' cannot be tracked. Make sure to have a problem matcher defined.", taskId) - : nls.localize('taskNotTracked', "The task '{0}' cannot be tracked. Make sure to have a problem matcher defined.", JSON.stringify(taskId)); - e({ severity: severity.Error, message: errorMessage }); - } - }, waitTime); + setTimeout(() => { + if (!taskStarted) { + const errorMessage = typeof taskId === 'string' + ? nls.localize('taskNotTrackedWithTaskId', "The task '{0}' cannot be tracked. Make sure to have a problem matcher defined.", taskId) + : nls.localize('taskNotTracked', "The task '{0}' cannot be tracked. Make sure to have a problem matcher defined.", JSON.stringify(taskId)); + e({ severity: severity.Error, message: errorMessage }); + } + }, waitTime); + }); }); } } diff --git a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts index a352f8e829c..8fd50b5d573 100644 --- a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts @@ -25,15 +25,14 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; -import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; +import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; import { distinct } from 'vs/base/common/arrays'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { disposableTimeout } from 'vs/base/common/async'; -import { isWeb } from 'vs/base/common/platform'; -import { IFileService } from 'vs/platform/files/common/files'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { ViewContainerLocation } from 'vs/workbench/common/views'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; type FileExtensionSuggestionClassification = { userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; @@ -103,8 +102,8 @@ export class FileBasedRecommendations extends ExtensionRecommendations { @IStorageService private readonly storageService: IStorageService, @IExtensionRecommendationNotificationService private readonly extensionRecommendationNotificationService: IExtensionRecommendationNotificationService, @IExtensionIgnoredRecommendationsService private readonly extensionIgnoredRecommendationsService: IExtensionIgnoredRecommendationsService, - @IFileService private readonly fileService: IFileService, - @ITASExperimentService private tasExperimentService: ITASExperimentService, + @IWorkbenchAssignmentService private tasExperimentService: IWorkbenchAssignmentService, + @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, ) { super(); @@ -166,18 +165,9 @@ export class FileBasedRecommendations extends ExtensionRecommendations { return; } - /* In Web, recommend only when the file can be handled */ - if (isWeb) { - if (!this.fileService.hasProvider(uri)) { - return; - } - } - - /* In Desktop, recommend only for files with these schemes */ - else { - if (![Schemas.untitled, Schemas.file, Schemas.vscodeRemote].includes(uri.scheme)) { - return; - } + const supportedSchemes = distinct([Schemas.untitled, Schemas.file, Schemas.vscodeRemote, ...this.workspaceContextService.getWorkspace().folders.map(folder => folder.uri.scheme)]); + if (!uri || !supportedSchemes.includes(uri.scheme)) { + return; } this.promptRecommendationsForModel(model); diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 732be379785..af890396dd6 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -34,7 +34,6 @@ import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; import { FileEditorInputSerializer, FileEditorWorkingCopyEditorHandler } from 'vs/workbench/contrib/files/browser/editors/fileEditorHandler'; import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; -import product from 'vs/platform/product/common/product'; class FileUriLabelContribution implements IWorkbenchContribution { @@ -240,6 +239,7 @@ configurationRegistry.registerConfiguration({ 'files.autoSaveDelay': { 'type': 'number', 'default': 1000, + 'minimum': 0, 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSaveDelay' }, "Controls the delay in milliseconds after which an editor with unsaved changes is saved automatically. Only applies when `#files.autoSave#` is set to `{0}`.", AutoSaveConfiguration.AFTER_DELAY) }, 'files.watcherExclude': { @@ -267,7 +267,7 @@ configurationRegistry.registerConfiguration({ 'markdownEnumDescriptions': [ nls.localize('files.legacyWatcher.on', "Enable the legacy file watcher in case you see issues with the new file watcher."), nls.localize('files.legacyWatcher.off', "Disable the legacy file watcher and enable the new file watcher to benefit from its capabilities."), - nls.localize('files.legacyWatcher.default', "The new file watcher will be enabled if you are using insiders version or whenever you open multi-root workspaces."), + nls.localize('files.legacyWatcher.default', "The new file watcher will be enabled."), ], 'default': 'default', 'description': nls.localize('legacyWatcher', "Controls the mechanism used for file watching. Only change this when you see issues related to file watching."), @@ -311,7 +311,7 @@ configurationRegistry.registerConfiguration({ 'files.experimentalSandboxedFileService': { 'type': 'boolean', 'description': nls.localize('files.experimentalSandboxedFileService', "Experimental: changes the file service to be sandboxed. Do not change this unless instructed!"), - 'default': product.quality !== 'stable', + 'default': true, 'scope': ConfigurationScope.APPLICATION }, } diff --git a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts index bda8894840d..469c0cf0e64 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts @@ -100,7 +100,17 @@ Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfigurat 'description': Messages.PROBLEMS_PANEL_CONFIGURATION_SHOW_CURRENT_STATUS, 'type': 'boolean', 'default': false - } + }, + 'problems.compareOrder': { + 'description': Messages.PROBLEMS_PANEL_CONFIGURATION_COMPARE_ORDER, + 'type': 'string', + 'default': ['severity'], + 'enum': ['severity', 'position'], + 'enumDescriptions': [ + Messages.PROBLEMS_PANEL_CONFIGURATION_COMPARE_ORDER_SEVERITY, + Messages.PROBLEMS_PANEL_CONFIGURATION_COMPARE_ORDER_POSITION, + ], + }, } }); diff --git a/src/vs/workbench/contrib/markers/browser/messages.ts b/src/vs/workbench/contrib/markers/browser/messages.ts index d263f7e1078..3e615d39368 100644 --- a/src/vs/workbench/contrib/markers/browser/messages.ts +++ b/src/vs/workbench/contrib/markers/browser/messages.ts @@ -16,6 +16,9 @@ export default class Messages { public static PROBLEMS_PANEL_CONFIGURATION_TITLE: string = nls.localize('problems.panel.configuration.title', "Problems View"); public static PROBLEMS_PANEL_CONFIGURATION_AUTO_REVEAL: string = nls.localize('problems.panel.configuration.autoreveal', "Controls whether Problems view should automatically reveal files when opening them."); public static PROBLEMS_PANEL_CONFIGURATION_SHOW_CURRENT_STATUS: string = nls.localize('problems.panel.configuration.showCurrentInStatus', "When enabled shows the current problem in the status bar."); + public static PROBLEMS_PANEL_CONFIGURATION_COMPARE_ORDER: string = nls.localize('problems.panel.configuration.compareOrder', "Controls the order in which problems are navigated."); + public static PROBLEMS_PANEL_CONFIGURATION_COMPARE_ORDER_SEVERITY: string = nls.localize('problems.panel.configuration.compareOrder.severity', "Navigate problems ordered by severity"); + public static PROBLEMS_PANEL_CONFIGURATION_COMPARE_ORDER_POSITION: string = nls.localize('problems.panel.configuration.compareOrder.position', "Navigate problems ordered by position"); public static MARKERS_PANEL_TITLE_PROBLEMS: string = nls.localize('markers.panel.title.problems', "Problems"); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts index 5c296c28d06..77554cd2187 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts @@ -193,7 +193,7 @@ registerAction2(class extends Action2 { // Next display all of the kernels grouped by categories or extensions. // If we don't have a kind, always display those at the bottom. - const picks = all.filter(item => item !== selected && !suggestions.includes(item)).map(toQuickPick); + const picks = all.filter(item => !suggestions.includes(item)).map(toQuickPick); const kernelsPerCategory = groupBy(picks, (a, b) => compareIgnoreCase(a.kernel.kind || 'z', b.kernel.kind || 'z')); kernelsPerCategory.forEach(items => { quickPickItems.push({ diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider.ts b/src/vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider.ts index dd92f64d1fb..080aea7bcf2 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider.ts @@ -10,6 +10,7 @@ import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } fr import { IMarkerListProvider, MarkerList, IMarkerNavigationService } from 'vs/editor/contrib/gotoError/markerNavigationService'; import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IMarkerService } from 'vs/platform/markers/common/markers'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IDisposable } from 'vs/base/common/lifecycle'; class MarkerListProvider implements IMarkerListProvider { @@ -19,6 +20,7 @@ class MarkerListProvider implements IMarkerListProvider { constructor( @IMarkerService private readonly _markerService: IMarkerService, @IMarkerNavigationService markerNavigation: IMarkerNavigationService, + @IConfigurationService private readonly _configService: IConfigurationService, ) { this._dispoables = markerNavigation.registerProvider(this); } @@ -38,7 +40,7 @@ class MarkerListProvider implements IMarkerListProvider { return new MarkerList(uri => { const otherData = CellUri.parse(uri); return otherData?.notebook.toString() === data.notebook.toString(); - }, this._markerService); + }, this._markerService, this._configService); } } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/profile/notebookProfile.ts b/src/vs/workbench/contrib/notebook/browser/contrib/profile/notebookProfile.ts index 811b6796a30..090e1383270 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/profile/notebookProfile.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/profile/notebookProfile.ts @@ -10,7 +10,7 @@ import { localize } from 'vs/nls'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { CellToolbarLocation, CompactView, ConsolidatedRunButton, FocusIndicator, GlobalToolbar, InsertToolbarLocation, ShowCellStatusBar, UndoRedoPerCell } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; +import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -92,7 +92,7 @@ function isSetProfileArgs(args: unknown): args is ISetProfileArgs { } export class NotebookProfileContribution extends Disposable { - constructor(@IConfigurationService configService: IConfigurationService, @ITASExperimentService private readonly experimentService: ITASExperimentService) { + constructor(@IConfigurationService configService: IConfigurationService, @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService) { super(); if (this.experimentService) { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts index 1d2e892001c..771b9a3c060 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts @@ -119,7 +119,7 @@ export class DiffNestedCellViewModel extends Disposable implements IDiffNestedCe this._ensureOutputsTop(); this._outputCollection[index] = height; - if (this._outputsTop!.changeValue(index, height)) { + if (this._outputsTop!.setValue(index, height)) { this._onDidChangeOutputLayout.fire(); } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 2fa0421ca7f..041bf2dbb26 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -12,7 +12,7 @@ import { Range } from 'vs/editor/common/core/range'; import { FindMatch, IModelDeltaDecoration, IReadonlyTextBuffer, ITextModel } from 'vs/editor/common/model'; import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; -import { CellViewModel, IModelDecorationsChangeAccessor, INotebookViewCellsUpdateEvent, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { CellViewModel, IModelDecorationsChangeAccessor, INotebookEditorViewState, INotebookViewCellsUpdateEvent, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { CellKind, NotebookCellMetadata, IOrderedMimeType, INotebookRendererInfo, ICellOutput, INotebookCellStatusBarItem, NotebookCellInternalMetadata, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange, cellRangesToIndexes, reduceCellRanges } from 'vs/workbench/contrib/notebook/common/notebookRange'; @@ -325,6 +325,7 @@ export interface INotebookEditorOptions extends ITextEditorOptions { readonly cellOptions?: ITextResourceEditorInput; readonly cellSelections?: ICellRange[]; readonly isReadOnly?: boolean; + readonly viewState?: INotebookEditorViewState; } export type INotebookEditorContributionCtor = IConstructorSignature1<INotebookEditor, INotebookEditorContribution>; @@ -398,6 +399,7 @@ export interface INotebookEditor { readonly onDidFocusEditorWidget: Event<void>; readonly onDidScroll: Event<void>; readonly onDidChangeActiveCell: Event<void>; + readonly onDidChangeActiveKernel: Event<void>; readonly onMouseUp: Event<INotebookEditorMouseEvent>; readonly onMouseDown: Event<INotebookEditorMouseEvent>; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index d8476a81342..b030a953c2f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -223,7 +223,7 @@ export class NotebookEditor extends EditorPane { - const viewState = this._loadNotebookEditorViewState(input); + const viewState = options?.viewState ?? this._loadNotebookEditorViewState(input); this._widget.value?.setParentContextKeyService(this._contextKeyService); await this._widget.value!.setModel(model.notebook, viewState); @@ -287,7 +287,7 @@ export class NotebookEditor extends EditorPane { editorLoaded: editorLoaded - startTime }); } else { - console.warn('notebook file open perf marks are broken'); + console.warn(`notebook file open perf marks are broken: startTime ${startTime}, extensionActiviated ${extensionActivated}, inputLoaded ${inputLoaded}, customMarkdownLoaded ${customMarkdownLoaded}, editorLoaded ${editorLoaded}`); } } } @@ -312,6 +312,17 @@ export class NotebookEditor extends EditorPane { super.saveState(); } + override getViewState(): INotebookEditorViewState | undefined { + const input = this.input; + if (!(input instanceof NotebookEditorInput)) { + return undefined; + } + + this._saveEditorViewState(input); + return this._loadNotebookEditorViewState(input); + } + + private _saveEditorViewState(input: EditorInput | undefined): void { if (this.group && this._widget.value && input instanceof NotebookEditorInput) { if (this._widget.value.isDisposed) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 38057ac1322..c0cda5dfc48 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -248,6 +248,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD readonly onDidBlur = this._onDidBlurEmitter.event; private readonly _onDidChangeActiveEditor = this._register(new Emitter<this>()); readonly onDidChangeActiveEditor: Event<this> = this._onDidChangeActiveEditor.event; + private readonly _onDidChangeActiveKernel = this._register(new Emitter<void>()); + readonly onDidChangeActiveKernel: Event<void> = this._onDidChangeActiveKernel.event; private readonly _onMouseUp: Emitter<INotebookEditorMouseEvent> = this._register(new Emitter<INotebookEditorMouseEvent>()); readonly onMouseUp: Event<INotebookEditorMouseEvent> = this._onMouseUp.event; private readonly _onMouseDown: Emitter<INotebookEditorMouseEvent> = this._register(new Emitter<INotebookEditorMouseEvent>()); @@ -385,6 +387,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._register(notebookKernelService.onDidChangeSelectedNotebooks(e => { if (isEqual(e.notebook, this.viewModel?.uri)) { this._loadKernelPreloads(); + this._onDidChangeActiveKernel.fire(); } })); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 4004699a49d..2a934aa89c0 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -174,7 +174,7 @@ export class NotebookProviderInfoStore extends Disposable { cellOptions = { resource, options }; } - const notebookOptions: INotebookEditorOptions = { ...options, cellOptions }; + const notebookOptions = { ...options, cellOptions } as INotebookEditorOptions; return { editor: NotebookEditorInput.create(this._instantiationService, notebookUri, notebookProviderInfo.id), options: notebookOptions }; }; const notebookUntitledEditorFactory: UntitledEditorInputFactoryFunction = async ({ resource, options }) => { @@ -314,8 +314,8 @@ export class NotebookOutputRendererInfoStore { const enum ReuseOrder { PreviouslySelected = 1 << 8, SameExtensionAsNotebook = 2 << 8, - BuiltIn = 3 << 8, - OtherRenderer = 4 << 8 + OtherRenderer = 3 << 8, + BuiltIn = 4 << 8, } const preferred = notebookProviderInfo && this.preferredMimetype.getValue()[notebookProviderInfo.id]?.[mimeType]; @@ -333,7 +333,7 @@ export class NotebookOutputRendererInfoStore { ? ReuseOrder.PreviouslySelected : renderer.extensionId.value === notebookProviderInfo?.extension?.value ? ReuseOrder.SameExtensionAsNotebook - : ReuseOrder.BuiltIn; + : renderer.isBuiltin ? ReuseOrder.BuiltIn : ReuseOrder.OtherRenderer; return { ordered: { mimeType, rendererId: renderer.id, isTrusted: true }, score: reuseScore | ownScore, diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index e64c3bc5687..bf760e78cf9 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -718,6 +718,12 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende } })); + disposables.add(this.notebookEditor.onDidChangeActiveKernel(() => { + if (templateData.currentRenderedCell) { + this.updateForKernel(templateData.currentRenderedCell as CodeCellViewModel, templateData); + } + })); + this.commonRenderTemplate(templateData); return templateData; @@ -919,6 +925,10 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende } } + private updateForKernel(element: CodeCellViewModel, templateData: CodeCellRenderTemplate): void { + this.updateExecutionOrder(element.internalMetadata, templateData); + } + private updateExecutionOrder(internalMetadata: NotebookCellInternalMetadata, templateData: CodeCellRenderTemplate): void { if (this.notebookEditor.activeKernel?.implementsExecutionOrder) { const executionOrderLabel = typeof internalMetadata.executionOrder === 'number' ? @@ -1046,6 +1056,8 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende this.updateForOutputs(element, templateData); elementDisposables.add(element.onDidChangeOutputs(_e => this.updateForOutputs(element, templateData))); + this.updateForKernel(element, templateData); + this.setupCellToolbarActions(templateData, elementDisposables); const toolbarContext = <INotebookCellActionContext>{ @@ -1149,7 +1161,7 @@ export class ListTopCellToolbar extends Disposable { } private updateClass() { - if (this.notebookEditor.getLength() === 0) { + if (this.notebookEditor.hasModel() && this.notebookEditor.getLength() === 0) { this.topCellToolbar.classList.add('emptyNotebook'); } else { this.topCellToolbar.classList.remove('emptyNotebook'); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts index a64db2d8ca6..638bf5f0d67 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts @@ -70,14 +70,6 @@ export class StatefulMarkdownCell extends Disposable { this._register(toDisposable(() => renderedEditors.delete(this.viewCell))); - this._register(viewCell.onDidChangeState((e) => { - if (e.editStateChanged) { - this.viewUpdate(); - } else if (e.contentChanged) { - this.viewUpdate(); - } - })); - this._register(viewCell.model.onDidChangeMetadata(() => { this.viewUpdate(); })); @@ -89,15 +81,27 @@ export class StatefulMarkdownCell extends Disposable { templateData.container.classList.toggle('cell-editor-focus', viewCell.focusMode === CellFocusMode.Editor); }; + this._register(viewCell.onDidChangeState((e) => { - if (!e.focusModeChanged) { - return; + if (e.editStateChanged || e.contentChanged) { + this.viewUpdate(); } - updateForFocusMode(); - })); - updateForFocusMode(); + if (e.focusModeChanged) { + updateForFocusMode(); + } + if (e.foldingStateChanged) { + const foldingState = viewCell.foldingState; + + if (foldingState !== this.foldingState) { + this.foldingState = foldingState; + this.setFoldingIndicator(); + } + } + })); + + updateForFocusMode(); this.foldingState = viewCell.foldingState; this.setFoldingIndicator(); this.updateFoldingIconShowClass(); @@ -108,19 +112,6 @@ export class StatefulMarkdownCell extends Disposable { } })); - this._register(viewCell.onDidChangeState((e) => { - if (!e.foldingStateChanged) { - return; - } - - const foldingState = viewCell.foldingState; - - if (foldingState !== this.foldingState) { - this.foldingState = foldingState; - this.setFoldingIndicator(); - } - })); - this._register(viewCell.onDidChangeLayout((e) => { const layoutInfo = this.editor?.getLayoutInfo(); if (e.outerWidth && this.viewCell.getEditState() === CellEditState.Editing && layoutInfo && layoutInfo.width !== viewCell.layoutInfo.editorWidth) { @@ -265,7 +256,6 @@ export class StatefulMarkdownCell extends Disposable { } this.editor!.setModel(model); - this.focusEditorIfNeeded(); const realContentHeight = this.editor!.getContentHeight(); if (realContentHeight !== editorHeight) { @@ -314,10 +304,21 @@ export class StatefulMarkdownCell extends Disposable { } private focusEditorIfNeeded() { - if ( - this.viewCell.focusMode === CellFocusMode.Editor && - (this.notebookEditor.hasEditorFocus() || document.activeElement === document.body)) { // Don't steal focus from other workbench parts, but if body has focus, we can take it - this.editor?.focus(); + if (this.viewCell.focusMode === CellFocusMode.Editor && + (this.notebookEditor.hasEditorFocus() || document.activeElement === document.body) + ) { // Don't steal focus from other workbench parts, but if body has focus, we can take it + if (!this.editor) { + return; + } + + this.editor.focus(); + + const primarySelection = this.editor.getSelection(); + if (!primarySelection) { + return; + } + + this.notebookEditor.revealRangeInViewAsync(this.viewCell, primarySelection); } } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 6c08612134c..31592764262 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -7,6 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable, IReference } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IPosition } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; @@ -16,6 +17,7 @@ import { SearchParams } from 'vs/editor/common/model/textModelSearch'; import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { IWordWrapTransientState, readTransientState, writeTransientState } from 'vs/workbench/contrib/codeEditor/browser/toggleWordWrap'; import { CellEditState, CellFocusMode, CellViewModelStateChangeEvent, CursorAtBoundary, IEditableCellViewModel, INotebookCellDecorationOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; @@ -112,6 +114,7 @@ export abstract class BaseCellViewModel extends Disposable { } private _editorListeners: IDisposable[] = []; private _editorViewStates: editorCommon.ICodeEditorViewState | null = null; + private _editorTransientState: IWordWrapTransientState | null = null; private _resolvedCellDecorations = new Map<string, INotebookCellDecorationOptions>(); private readonly _cellDecorationsChanged = this._register(new Emitter<{ added: INotebookCellDecorationOptions[], removed: INotebookCellDecorationOptions[] }>()); @@ -155,6 +158,7 @@ export abstract class BaseCellViewModel extends Disposable { private readonly _configurationService: IConfigurationService, private readonly _modelService: ITextModelService, private readonly _undoRedoService: IUndoRedoService, + private readonly _codeEditorService: ICodeEditorService, // private readonly _keymapService: INotebookKeymapService ) { super(); @@ -219,6 +223,10 @@ export abstract class BaseCellViewModel extends Disposable { this._restoreViewState(this._editorViewStates); } + if (this._editorTransientState) { + writeTransientState(editor.getModel(), this._editorTransientState, this._codeEditorService); + } + this._resolvedDecorations.forEach((value, key) => { if (key.startsWith('_lazy_')) { // lazy ones @@ -239,6 +247,7 @@ export abstract class BaseCellViewModel extends Disposable { detachTextEditor() { this.saveViewState(); + this.saveTransientState(); // decorations need to be cleared first as editors can be resued. this._resolvedDecorations.forEach(value => { const resolvedid = value.id; @@ -275,6 +284,14 @@ export abstract class BaseCellViewModel extends Disposable { this._editorViewStates = this._textEditor.saveViewState(); } + private saveTransientState() { + if (!this._textEditor || !this._textEditor.hasModel()) { + return; + } + + this._editorTransientState = readTransientState(this._textEditor.getModel(), this._codeEditorService); + } + saveEditorViewState() { if (this._textEditor) { this._editorViewStates = this._textEditor.saveViewState(); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index eadd68541a7..873d760d39d 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -6,6 +6,7 @@ import { Emitter, Event, PauseableEmitter } from 'vs/base/common/event'; import { dispose } from 'vs/base/common/lifecycle'; import * as UUID from 'vs/base/common/uuid'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; @@ -111,9 +112,10 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod @INotebookService private readonly _notebookService: INotebookService, @ITextModelService modelService: ITextModelService, @IUndoRedoService undoRedoService: IUndoRedoService, - @INotebookKeymapService keymapService: INotebookKeymapService + @INotebookKeymapService keymapService: INotebookKeymapService, + @ICodeEditorService codeEditorService: ICodeEditorService ) { - super(viewType, model, UUID.generateUuid(), viewContext, configurationService, modelService, undoRedoService); + super(viewType, model, UUID.generateUuid(), viewContext, configurationService, modelService, undoRedoService, codeEditorService); this._outputViewModels = this.model.outputs.map(output => new CellOutputViewModel(this, output, this._notebookService)); this._register(this.model.onDidChangeOutputs((splice) => { @@ -383,7 +385,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod } this._outputCollection[index] = height; - if (this._outputsTop!.changeValue(index, height)) { + if (this._outputsTop!.setValue(index, height)) { this.layoutChange({ outputHeight: true }, source); } } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts index f6b77589525..f7183951266 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts @@ -17,6 +17,7 @@ import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/vie import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/common/notebookOptions'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewModel { @@ -112,8 +113,9 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM @ITextModelService textModelService: ITextModelService, @IInstantiationService instantiationService: IInstantiationService, @IUndoRedoService undoRedoService: IUndoRedoService, + @ICodeEditorService codeEditorService: ICodeEditorService ) { - super(viewType, model, UUID.generateUuid(), viewContext, configurationService, textModelService, undoRedoService); + super(viewType, model, UUID.generateUuid(), viewContext, configurationService, textModelService, undoRedoService, codeEditorService); const { bottomToolbarGap } = this.viewContext.notebookOptions.computeBottomToolbarDimensions(this.viewType); diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts index 5f148fa4b91..8e1f4b086e7 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts @@ -25,7 +25,7 @@ import { NotebooKernelActionViewItem } from 'vs/workbench/contrib/notebook/brows import { ActionViewWithLabel } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellActionView'; import { GlobalToolbarShowLabel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; +import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; interface IActionModel { @@ -69,7 +69,7 @@ export class NotebookEditorToolbar extends Disposable { @IMenuService readonly menuService: IMenuService, @IEditorService private readonly editorService: IEditorService, @IKeybindingService private readonly keybindingService: IKeybindingService, - @ITASExperimentService private readonly experimentService: ITASExperimentService + @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService ) { super(); diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts index 2205ff78222..238c38653a2 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as glob from 'vs/base/common/glob'; -import { GroupIdentifier, ISaveOptions, IMoveResult, IRevertOptions, EditorInputCapabilities, Verbosity, IUntypedEditorInput } from 'vs/workbench/common/editor'; +import { GroupIdentifier, ISaveOptions, IMoveResult, IRevertOptions, EditorInputCapabilities, Verbosity, IUntypedEditorInput, isResourceDiffEditorInput, isResourceSideBySideEditorInput } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService'; import { URI } from 'vs/base/common/uri'; @@ -13,7 +13,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { IDisposable, IReference } from 'vs/base/common/lifecycle'; -import { CellEditType, IResolvedNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellUri, IResolvedNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ILabelService } from 'vs/platform/label/common/label'; import { Schemas } from 'vs/base/common/network'; import { mark } from 'vs/workbench/contrib/notebook/common/notebookPerformance'; @@ -285,6 +285,12 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { if (otherInput instanceof NotebookEditorInput) { return this.viewType === otherInput.viewType && isEqual(this.resource, otherInput.resource); } + if (isResourceDiffEditorInput(otherInput) || isResourceSideBySideEditorInput(otherInput)) { + return false; + } + if (otherInput.resource && otherInput.resource.scheme === Schemas.vscodeNotebookCell) { + return isEqual(this.resource, CellUri.parse(otherInput.resource)?.notebook); + } return false; } } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index b4aafa2d753..f699cb4d5dc 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -596,14 +596,18 @@ function isObjectSetting({ return false; } - // object additional properties allow it to have any shape - if (objectAdditionalProperties === true || objectAdditionalProperties === undefined) { + // objectAdditionalProperties allow the setting to have any shape, + // but if there's a pattern property that handles everything, then every + // property will match that patternProperty, so we don't need to look at + // the value of objectAdditionalProperties in that case. + if ((objectAdditionalProperties === true || objectAdditionalProperties === undefined) + && !Object.keys(objectPatternProperties ?? {}).includes('.*')) { return false; } const schemas = [...Object.values(objectProperties ?? {}), ...Object.values(objectPatternProperties ?? {})]; - if (typeof objectAdditionalProperties === 'object') { + if (objectAdditionalProperties && typeof objectAdditionalProperties === 'object') { schemas.push(objectAdditionalProperties); } diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index d7dd132c96b..03a9183ba42 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -50,6 +50,7 @@ import { EncodingMode, ITextFileEditorModel, IResolvedTextFileEditorModel, IText import { gotoNextLocation, gotoPreviousLocation } from 'vs/platform/theme/common/iconRegistry'; import { Codicon } from 'vs/base/common/codicons'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { TextCompareEditorActiveContext } from 'vs/workbench/common/editor'; class DiffActionRunner extends ActionRunner { @@ -363,7 +364,7 @@ export class ShowPreviousChangeAction extends EditorAction { id: 'editor.action.dirtydiff.previous', label: nls.localize('show previous change', "Show Previous Change"), alias: 'Show Previous Change', - precondition: undefined, + precondition: TextCompareEditorActiveContext.toNegated(), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F3, weight: KeybindingWeight.EditorContrib } }); } @@ -397,7 +398,7 @@ export class ShowNextChangeAction extends EditorAction { id: 'editor.action.dirtydiff.next', label: nls.localize('show next change', "Show Next Change"), alias: 'Show Next Change', - precondition: undefined, + precondition: TextCompareEditorActiveContext.toNegated(), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Alt | KeyCode.F3, weight: KeybindingWeight.EditorContrib } }); } @@ -443,14 +444,14 @@ MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { order: 2 }); -export class MoveToPreviousChangeAction extends EditorAction { +export class GotoPreviousChangeAction extends EditorAction { constructor() { super({ id: 'workbench.action.editor.previousChange', - label: nls.localize('move to previous change', "Move to Previous Change"), - alias: 'Move to Previous Change', - precondition: undefined, + label: nls.localize('move to previous change', "Go to Previous Change"), + alias: 'Go to Previous Change', + precondition: TextCompareEditorActiveContext.toNegated(), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F5, weight: KeybindingWeight.EditorContrib } }); } @@ -483,16 +484,16 @@ export class MoveToPreviousChangeAction extends EditorAction { outerEditor.revealPositionInCenter(position); } } -registerEditorAction(MoveToPreviousChangeAction); +registerEditorAction(GotoPreviousChangeAction); -export class MoveToNextChangeAction extends EditorAction { +export class GotoNextChangeAction extends EditorAction { constructor() { super({ id: 'workbench.action.editor.nextChange', - label: nls.localize('move to next change', "Move to Next Change"), - alias: 'Move to Next Change', - precondition: undefined, + label: nls.localize('move to next change', "Go to Next Change"), + alias: 'Go to Next Change', + precondition: TextCompareEditorActiveContext.toNegated(), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Alt | KeyCode.F5, weight: KeybindingWeight.EditorContrib } }); } @@ -525,7 +526,7 @@ export class MoveToNextChangeAction extends EditorAction { outerEditor.revealPositionInCenter(position); } } -registerEditorAction(MoveToNextChangeAction); +registerEditorAction(GotoNextChangeAction); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'closeDirtyDiff', diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 0c2a5f11323..be9ec9e5944 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -132,10 +132,6 @@ margin-right: 3px; } -.scm-view .monaco-list-row .resource > .name.strike-through > .monaco-icon-label > .monaco-icon-label-container > .monaco-icon-name-container > .label-name { - text-decoration: line-through; -} - .scm-view .monaco-list-row .resource > .decoration-icon { width: 16px; height: 100%; @@ -170,6 +166,13 @@ display: block; } +.scm-view .monaco-list .monaco-list-row.force-no-hover, +.scm-view .monaco-list .monaco-list-row:hover.force-no-hover, +.scm-view .monaco-list .monaco-list-row.focused.force-no-hover, +.scm-view .monaco-list .monaco-list-row.selected.force-no-hover { + background: transparent !important; +} + .scm-view.show-actions .scm-provider > .actions, .scm-view.show-actions > .monaco-list .monaco-list-row .resource-group > .actions, .scm-view.show-actions > .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions { @@ -195,14 +198,15 @@ } .scm-view .button-container { - margin-left: 8px; + padding-left: 11px; height: 100%; display: flex; align-items: center; } -.scm-view .button-container .codicon { - margin: 0 0.4em; +.scm-view .button-container .codicon.codicon-cloud-upload, +.scm-view .button-container .codicon.codicon-sync { + margin: 0 0.4em 0 0; } .scm-view .button-container .codicon.codicon-arrow-up, diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index b26222b3da9..762a0fc98fb 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -113,6 +113,9 @@ class ActionButtonRenderer implements ICompressibleTreeRenderer<ISCMActionButton ) { } renderTemplate(container: HTMLElement): ActionButtonTemplate { + // Disable hover for list item + container.parentElement!.parentElement!.classList.add('force-no-hover'); + const buttonContainer = append(container, $('.button-container')); const actionButton = new ScmActionButton(buttonContainer, this.commandService, this.themeService, this.notificationService); @@ -167,6 +170,9 @@ class InputRenderer implements ICompressibleTreeRenderer<ISCMInput, FuzzyScore, // hack (container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-no-twistie'); + // Disable hover for list item + container.parentElement!.parentElement!.classList.add('force-no-hover'); + const disposables = new DisposableStore(); const inputElement = append(container, $('.scm-input')); const inputWidget = this.instantiationService.createInstance(SCMInputWidget, inputElement, this.overflowWidgetsDomNode); @@ -409,26 +415,26 @@ class ResourceRenderer implements ICompressibleTreeRenderer<ISCMResource | IReso let matches: IMatch[] | undefined; let descriptionMatches: IMatch[] | undefined; + let strikethrough: boolean | undefined; if (ResourceTree.isResourceNode(resourceOrFolder)) { if (resourceOrFolder.element) { const menus = this.scmViewService.menus.getRepositoryMenus(resourceOrFolder.element.resourceGroup.provider); elementDisposables.add(connectPrimaryMenuToInlineActionBar(menus.getResourceMenu(resourceOrFolder.element), template.actionBar)); - template.name.classList.toggle('strike-through', resourceOrFolder.element.decorations.strikeThrough); template.element.classList.toggle('faded', resourceOrFolder.element.decorations.faded); + strikethrough = resourceOrFolder.element.decorations.strikeThrough; } else { matches = createMatches(node.filterData as FuzzyScore | undefined); const menus = this.scmViewService.menus.getRepositoryMenus(resourceOrFolder.context.provider); elementDisposables.add(connectPrimaryMenuToInlineActionBar(menus.getResourceFolderMenu(resourceOrFolder.context), template.actionBar)); - template.name.classList.remove('strike-through'); template.element.classList.remove('faded'); } } else { [matches, descriptionMatches] = this._processFilterData(uri, node.filterData); const menus = this.scmViewService.menus.getRepositoryMenus(resourceOrFolder.resourceGroup.provider); elementDisposables.add(connectPrimaryMenuToInlineActionBar(menus.getResourceMenu(resourceOrFolder), template.actionBar)); - template.name.classList.toggle('strike-through', resourceOrFolder.decorations.strikeThrough); template.element.classList.toggle('faded', resourceOrFolder.decorations.faded); + strikethrough = resourceOrFolder.decorations.strikeThrough; } const render = () => { @@ -440,7 +446,8 @@ class ResourceRenderer implements ICompressibleTreeRenderer<ISCMResource | IReso hidePath: viewModel.mode === ViewModelMode.Tree, fileKind, matches, - descriptionMatches + descriptionMatches, + strikethrough }); if (icon) { diff --git a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts index 48b87fc97b2..7f8c3836853 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts @@ -72,81 +72,61 @@ export class SnippetCompletionProvider implements CompletionItemProvider { const sw = new StopWatch(true); const languageId = this._getLanguageIdAtPosition(model, position); - const snippets = await this._snippets.getSnippets(languageId); + const snippets = new Set(await this._snippets.getSnippets(languageId)); - let pos = { lineNumber: position.lineNumber, column: 1 }; - let lineOffsets: number[] = []; - const lineContent = model.getLineContent(position.lineNumber).toLowerCase(); - const endsInWhitespace = /\s/.test(lineContent[position.column - 2]); + const lineContentLow = model.getLineContent(position.lineNumber).toLowerCase(); - while (pos.column < position.column) { - let word = model.getWordAtPosition(pos); - if (word) { - // at a word - lineOffsets.push(word.startColumn - 1); - pos.column = word.endColumn + 1; - if (word.endColumn < position.column && !/\s/.test(lineContent[word.endColumn - 1])) { - lineOffsets.push(word.endColumn - 1); - } - } - else if (!/\s/.test(lineContent[pos.column - 1])) { - // at a none-whitespace character - lineOffsets.push(pos.column - 1); - pos.column += 1; - } - else { - // always advance! - pos.column += 1; - } - } - - const availableSnippets = new Set<Snippet>(snippets); const suggestions: SnippetCompletion[] = []; - const columnOffset = position.column - 1; - for (const start of lineOffsets) { - availableSnippets.forEach(snippet => { - if (isPatternInWord(lineContent, start, columnOffset, snippet.prefixLow, 0, snippet.prefixLow.length)) { - const prefixPos = position.column - (1 + start); - const prefixRestLen = snippet.prefixLow.length - prefixPos; - const endsWithPrefixRest = compareSubstring(lineContent, snippet.prefixLow, columnOffset, (columnOffset) + prefixRestLen, prefixPos, prefixPos + prefixRestLen); - const startPosition = position.delta(0, -prefixPos); - let endColumn = endsWithPrefixRest === 0 ? position.column + prefixRestLen : position.column; + for (const snippet of snippets) { - // First check if there is anything to the right of the cursor - if (columnOffset < lineContent.length) { - const autoClosingPairs = LanguageConfigurationRegistry.getAutoClosingPairs(languageId); - const standardAutoClosingPairConditionals = autoClosingPairs.autoClosingPairsCloseSingleChar.get(lineContent[columnOffset]); - // If the character to the right of the cursor is a closing character of an autoclosing pair - if (standardAutoClosingPairConditionals?.some(p => - // and the start position is the opening character of an autoclosing pair - p.open === lineContent[startPosition.column - 1] && - // and the snippet prefix contains the opening and closing pair at its edges - snippet.prefix.startsWith(p.open) && - snippet.prefix[snippet.prefix.length - 1] === p.close)) { - - // Eat the character that was likely inserted because of auto-closing pairs - endColumn++; - } - } - - const replace = Range.fromPositions(startPosition, { lineNumber: position.lineNumber, column: endColumn }); - const insert = replace.setEndPosition(position.lineNumber, position.column); - - suggestions.push(new SnippetCompletion(snippet, { replace, insert })); - availableSnippets.delete(snippet); + for (let pos = Math.max(0, columnOffset - snippet.prefixLow.length); pos < lineContentLow.length; pos++) { + if (!isPatternInWord(lineContentLow, pos, columnOffset, snippet.prefixLow, 0, snippet.prefixLow.length)) { + continue; } - }); - } - if (endsInWhitespace || lineOffsets.length === 0) { - // add remaing snippets when the current prefix ends in whitespace or when no - // interesting positions have been found - availableSnippets.forEach(snippet => { - const insert = Range.fromPositions(position); - const replace = lineContent.indexOf(snippet.prefixLow, columnOffset) === columnOffset ? insert.setEndPosition(position.lineNumber, position.column + snippet.prefixLow.length) : insert; + + const prefixRestLen = snippet.prefixLow.length - (columnOffset - pos); + const endsWithPrefixRest = compareSubstring(lineContentLow, snippet.prefixLow, columnOffset, columnOffset + prefixRestLen, columnOffset - pos); + const startPosition = position.with(undefined, pos + 1); + let endColumn = endsWithPrefixRest === 0 ? position.column + prefixRestLen : position.column; + + // First check if there is anything to the right of the cursor + if (columnOffset < lineContentLow.length) { + const autoClosingPairs = LanguageConfigurationRegistry.getAutoClosingPairs(languageId); + const standardAutoClosingPairConditionals = autoClosingPairs.autoClosingPairsCloseSingleChar.get(lineContentLow[columnOffset]); + // If the character to the right of the cursor is a closing character of an autoclosing pair + if (standardAutoClosingPairConditionals?.some(p => + // and the start position is the opening character of an autoclosing pair + p.open === lineContentLow[startPosition.column - 1] && + // and the snippet prefix contains the opening and closing pair at its edges + snippet.prefix.startsWith(p.open) && + snippet.prefix[snippet.prefix.length - 1] === p.close) + ) { + // Eat the character that was likely inserted because of auto-closing pairs + endColumn++; + } + } + + const replace = Range.fromPositions(startPosition, { lineNumber: position.lineNumber, column: endColumn }); + const insert = replace.setEndPosition(position.lineNumber, position.column); + suggestions.push(new SnippetCompletion(snippet, { replace, insert })); - }); + snippets.delete(snippet); + break; + } + } + + + const endsInWhitespace = /\s/.test(lineContentLow[position.column - 2]); + + if (endsInWhitespace || !lineContentLow /*empty line*/) { + // add remaing snippets when the current prefix ends in whitespace or when line is empty + for (let snippet of snippets) { + const insert = Range.fromPositions(position); + const replace = lineContentLow.indexOf(snippet.prefixLow, columnOffset) === columnOffset ? insert.setEndPosition(position.lineNumber, position.column + snippet.prefixLow.length) : insert; + suggestions.push(new SnippetCompletion(snippet, { replace, insert })); + } } diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts index 94aae1bb795..73fc64c1756 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts @@ -12,7 +12,7 @@ import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets.contribution'; import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; -import { CompletionContext, CompletionTriggerKind } from 'vs/editor/common/modes'; +import { CompletionContext, CompletionItemLabel, CompletionItemRanges, CompletionTriggerKind } from 'vs/editor/common/modes'; import { DisposableStore } from 'vs/base/common/lifecycle'; class SimpleSnippetService implements ISnippetsService { @@ -91,6 +91,17 @@ suite('SnippetsService', function () { }); }); + test('snippet completions - simple 2', function () { + + const provider = new SnippetCompletionProvider(modeService, snippetService); + const model = disposables.add(createTextModel('hello ', undefined, 'fooLang')); + + return provider.provideCompletionItems(model, new Position(1, 6), context)!.then(result => { + assert.strictEqual(result.incomplete, undefined); + assert.strictEqual(result.suggestions.length, 2); + }); + }); + test('snippet completions - with prefix', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); @@ -139,24 +150,34 @@ suite('SnippetsService', function () { description: 'barTest' }); assert.strictEqual(result.suggestions[0].insertText, 's1'); - assert.strictEqual((result.suggestions[0].range as any).insert.startColumn, 1); + assert.strictEqual((result.suggestions[0].range as CompletionItemRanges).insert.startColumn, 1); assert.deepStrictEqual(result.suggestions[1].label, { label: 'bar-bar', description: 'name' }); assert.strictEqual(result.suggestions[1].insertText, 's2'); - assert.strictEqual((result.suggestions[1].range as any).insert.startColumn, 1); + assert.strictEqual((result.suggestions[1].range as CompletionItemRanges).insert.startColumn, 1); }); await provider.provideCompletionItems(model, new Position(1, 5), context)!.then(result => { assert.strictEqual(result.incomplete, undefined); - assert.strictEqual(result.suggestions.length, 1); - assert.deepStrictEqual(result.suggestions[0].label, { + assert.strictEqual(result.suggestions.length, 2); + + const [first, second] = result.suggestions; + + assert.deepStrictEqual(first.label, { + label: 'bar', + description: 'barTest' + }); + assert.strictEqual(first.insertText, 's1'); + assert.strictEqual((first.range as CompletionItemRanges).insert.startColumn, 5); + + assert.deepStrictEqual(second.label, { label: 'bar-bar', description: 'name' }); - assert.strictEqual(result.suggestions[0].insertText, 's2'); - assert.strictEqual((result.suggestions[0].range as any).insert.startColumn, 1); + assert.strictEqual(second.insertText, 's2'); + assert.strictEqual((second.range as CompletionItemRanges).insert.startColumn, 1); }); await provider.provideCompletionItems(model, new Position(1, 6), context)!.then(result => { @@ -550,4 +571,29 @@ suite('SnippetsService', function () { assert.strictEqual((first.range as any).replace.endColumn, 6); model.dispose(); }); + + test('Leading whitespace in snippet prefix #123860', async function () { + + snippetService = new SimpleSnippetService([new Snippet( + ['fooLang'], + 'cite-name', + ' cite', + '', + '~\\cite{$CLIPBOARD}', + '', + SnippetSource.User + )]); + + const provider = new SnippetCompletionProvider(modeService, snippetService); + + let model = createTextModel(' ci', undefined, 'fooLang'); + let result = await provider.provideCompletionItems(model, new Position(1, 4), context)!; + + assert.strictEqual(result.suggestions.length, 1); + let [first] = result.suggestions; + assert.strictEqual((<CompletionItemLabel>first.label).label, ' cite'); + assert.strictEqual((<CompletionItemRanges>first.range).insert.startColumn, 1); + + model.dispose(); + }); }); diff --git a/src/vs/workbench/contrib/surveys/browser/ces.contribution.ts b/src/vs/workbench/contrib/surveys/browser/ces.contribution.ts index 38a37e23c1d..125da89cf20 100644 --- a/src/vs/workbench/contrib/surveys/browser/ces.contribution.ts +++ b/src/vs/workbench/contrib/surveys/browser/ces.contribution.ts @@ -13,7 +13,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Severity, INotificationService } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; +import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; import { URI } from 'vs/base/common/uri'; import { platform } from 'vs/base/common/process'; import { ThrottledDelayer } from 'vs/base/common/async'; @@ -30,7 +30,7 @@ const REMIND_LATER_DATE_KEY = 'ces/remindLaterDate'; class CESContribution extends Disposable implements IWorkbenchContribution { private promptDelayer = this._register(new ThrottledDelayer<void>(0)); - private readonly tasExperimentService: ITASExperimentService | undefined; + private readonly tasExperimentService: IWorkbenchAssignmentService | undefined; constructor( @IStorageService private readonly storageService: IStorageService, @@ -38,7 +38,7 @@ class CESContribution extends Disposable implements IWorkbenchContribution { @ITelemetryService private readonly telemetryService: ITelemetryService, @IOpenerService private readonly openerService: IOpenerService, @IProductService private readonly productService: IProductService, - @ITASExperimentService tasExperimentService: ITASExperimentService, + @IWorkbenchAssignmentService tasExperimentService: IWorkbenchAssignmentService, ) { super(); diff --git a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts index 6bad0f57970..ecc77eb8a0f 100644 --- a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts +++ b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts @@ -87,6 +87,14 @@ const ModulesToLookFor = [ 'playwright-chromium', 'playwright-firefox', 'playwright-webkit', + // Other interesting browser testing packages + 'cypress', + 'nightwatch', + 'protractor', + 'puppeteer', + 'selenium-webdriver', + 'webdriverio', + 'gherkin', // AzureSDK packages '@azure/app-configuration', '@azure/cosmos-sign', @@ -303,8 +311,6 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.grunt" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.gulp" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.jake" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "workspace.devcontainer" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "workspace.docker" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.tsconfig" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.jsconfig" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.config.xml" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -366,7 +372,14 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.npm.playwright-core" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.playwright-chromium" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.playwright-firefox" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "workspace.npm.playwright-webkit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.playwright-webkit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.cypress" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.nightwatch" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.protractor" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.puppeteer" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.selenium-webdriver" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.webdriverio" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.gherkin" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/app-configuration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/cosmos-sign" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/cosmos-language-service" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -579,8 +592,6 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { tags['workspace.grunt'] = nameSet.has('gruntfile.js'); tags['workspace.gulp'] = nameSet.has('gulpfile.js'); tags['workspace.jake'] = nameSet.has('jakefile.js'); - tags['workspace.devcontainer'] = nameSet.has('devcontainer.json'); - tags['workspace.docker'] = nameSet.has('Dockerfile') || nameSet.has('docker-compose.yml'); tags['workspace.tsconfig'] = nameSet.has('tsconfig.json'); tags['workspace.jsconfig'] = nameSet.has('jsconfig.json'); diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 4353bdd449a..66258c6f7fe 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -926,16 +926,16 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer throw new TaskError(Severity.Info, nls.localize('TaskServer.noTask', 'Task to execute is undefined'), TaskErrors.TaskNotFound); } - // eslint-disable-next-line no-async-promise-executor - return new Promise<ITaskSummary | undefined>(async (resolve) => { + return new Promise<ITaskSummary | undefined>((resolve) => { let resolver = this.createResolver(); if (options && options.attachProblemMatcher && this.shouldAttachProblemMatcher(task) && !InMemoryTask.is(task)) { - const toExecute = await this.attachProblemMatcher(task); - if (toExecute) { - resolve(this.executeTask(toExecute, resolver, runSource)); - } else { - resolve(undefined); - } + this.attachProblemMatcher(task).then(toExecute => { + if (toExecute) { + resolve(this.executeTask(toExecute, resolver, runSource)); + } else { + resolve(undefined); + } + }); } else { resolve(this.executeTask(task, resolver, runSource)); } @@ -2361,10 +2361,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } }); - // eslint-disable-next-line no-async-promise-executor - const timeout: boolean = await Promise.race([new Promise<boolean>(async (resolve) => { - await _createEntries; - resolve(false); + const timeout: boolean = await Promise.race([new Promise<boolean>((resolve) => { + _createEntries.then(() => resolve(false)); }), new Promise<boolean>((resolve) => { const timer = setTimeout(() => { clearTimeout(timer); @@ -3036,10 +3034,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }); }); - // eslint-disable-next-line no-async-promise-executor - const timeout: boolean = await Promise.race([new Promise<boolean>(async (resolve) => { - await entries; - resolve(false); + const timeout: boolean = await Promise.race([new Promise<boolean>((resolve) => { + entries.then(() => resolve(false)); }), new Promise<boolean>((resolve) => { const timer = setTimeout(() => { clearTimeout(timer); diff --git a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts index 5add1d7d37f..4ad8c6aa2e6 100644 --- a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts +++ b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts @@ -20,6 +20,7 @@ import { ConfigurationTarget } from 'vs/platform/configuration/common/configurat import { IOpenerService } from 'vs/platform/opener/common/opener'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; +import { ILogService } from 'vs/platform/log/common/log'; const ARE_AUTOMATIC_TASKS_ALLOWED_IN_WORKSPACE = 'tasks.run.allowAutomatic'; @@ -27,22 +28,29 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut constructor( @ITaskService private readonly taskService: ITaskService, @IStorageService private readonly storageService: IStorageService, - @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService) { + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, + @ILogService private readonly logService: ILogService) { super(); this.tryRunTasks(); } private async tryRunTasks() { + this.logService.trace('RunAutomaticTasks: Trying to run tasks.'); // Wait until we have task system info (the extension host and workspace folders are available). if (!this.taskService.hasTaskSystemInfo) { + this.logService.trace('RunAutomaticTasks: Awaiting task system info.'); await Event.toPromise(Event.once(this.taskService.onDidChangeTaskSystemInfo)); } + + this.logService.trace('RunAutomaticTasks: Checking if automatic tasks should run.'); const isFolderAutomaticAllowed = this.storageService.getBoolean(ARE_AUTOMATIC_TASKS_ALLOWED_IN_WORKSPACE, StorageScope.WORKSPACE, undefined); const isWorkspaceTrusted = this.workspaceTrustManagementService.isWorkspaceTrusted(); // Only run if allowed. Prompting for permission occurs when a user first tries to run a task. if (isFolderAutomaticAllowed && isWorkspaceTrusted) { this.taskService.getWorkspaceTasks(TaskRunSource.FolderOpen).then(workspaceTaskResult => { let { tasks } = RunAutomaticTasks.findAutoTasks(this.taskService, workspaceTaskResult); + this.logService.trace(`RunAutomaticTasks: Found ${tasks.length} automatic tasks tasks`); + if (tasks.length > 0) { RunAutomaticTasks.runTasks(this.taskService, tasks); } diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index cccb96b47a2..5a19af8a42e 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -200,6 +200,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { private previousPanelId: string | undefined; private previousTerminalInstance: ITerminalInstance | undefined; private terminalStatusManager: TaskTerminalStatus; + private terminalCreationQueue: Promise<ITerminalInstance | void> = Promise.resolve(); private readonly _onDidStateChange: Emitter<TaskEvent>; @@ -1059,11 +1060,16 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { os, remoteAuthority: this.environmentService.remoteAuthority }); - const defaultConfig = { - shell: defaultProfile.path, - args: defaultProfile.args + shellLaunchConfig = { + name: terminalName, + description, + executable: defaultProfile.path, + args: defaultProfile.args, + icon: defaultProfile.icon, + env: { ...defaultProfile.env }, + color: defaultProfile.color, + waitOnExit }; - shellLaunchConfig = { name: terminalName, description, executable: defaultConfig.shell, args: defaultConfig.args, waitOnExit }; let shellSpecified: boolean = false; let shellOptions: ShellConfiguration | undefined = task.command.options && task.command.options.shell; if (shellOptions) { @@ -1189,13 +1195,36 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { shellLaunchConfig.cwd = isUNC(cwd) ? cwd : resources.toLocalResource(URI.from({ scheme: Schemas.file, path: cwd }), this.environmentService.remoteAuthority, this.pathService.defaultUriScheme); } if (options.env) { - shellLaunchConfig.env = options.env; + if (shellLaunchConfig.env) { + shellLaunchConfig.env = { ...shellLaunchConfig.env, ...options.env }; + } else { + shellLaunchConfig.env = options.env; + } } shellLaunchConfig.isFeatureTerminal = true; shellLaunchConfig.useShellEnvironment = true; return shellLaunchConfig; } + private async doCreateTerminal(group: string | undefined, launchConfigs: IShellLaunchConfig): Promise<ITerminalInstance> { + if (group) { + // Try to find an existing terminal to split. + // Even if an existing terminal is found, the split can fail if the terminal width is too small. + for (const terminal of values(this.terminals)) { + if (terminal.group === group) { + const originalInstance = terminal.terminal; + console.log('splitting'); + const result = await this.terminalService.createTerminal({ location: { parentTerminal: originalInstance }, config: launchConfigs }); + if (result) { + return result; + } + } + } + } + // Either no group is used, no terminal with the group exists or splitting an existing terminal failed. + return this.terminalService.createTerminal({ config: launchConfigs }); + } + private async createTerminal(task: CustomTask | ContributedTask, resolver: VariableResolver, workspaceFolder: IWorkspaceFolder | undefined): Promise<[ITerminalInstance | undefined, string | undefined, TaskError | undefined]> { let platform = resolver.taskSystemInfo ? resolver.taskSystemInfo.platform : Platform.platform; let options = await this.resolveOptions(resolver, task.command.options); @@ -1244,9 +1273,6 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { return [undefined, undefined, new TaskError(Severity.Error, nls.localize('TerminalTaskSystem', 'Can\'t execute a shell command on an UNC drive using cmd.exe.'), TaskErrors.UnknownError)]; } } - if (this.currentTask.shellLaunchConfig) { - this.currentTask.shellLaunchConfig.icon = { id: 'tools' }; - } let prefersSameTerminal = presentationOptions.panel === PanelKind.Dedicated; let allowsSharedTerminal = presentationOptions.panel === PanelKind.Shared; @@ -1288,31 +1314,16 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { await terminalToReuse.terminal.reuseTerminal(launchConfigs); if (task.command.presentation && task.command.presentation.clear) { - terminalToReuse.terminal.clear(); + terminalToReuse.terminal.clearBuffer(); } this.terminals[terminalToReuse.terminal.instanceId.toString()].lastTask = taskKey; return [terminalToReuse.terminal, commandExecutable, undefined]; } - let result: ITerminalInstance | null = null; - if (group) { - // Try to find an existing terminal to split. - // Even if an existing terminal is found, the split can fail if the terminal width is too small. - for (const terminal of values(this.terminals)) { - if (terminal.group === group) { - const originalInstance = terminal.terminal; - await originalInstance.waitForTitle(); - result = await this.terminalService.createTerminal({ location: { parentTerminal: originalInstance }, config: launchConfigs }); - if (result) { - break; - } - } - } - } - if (!result) { - // Either no group is used, no terminal with the group exists or splitting an existing terminal failed. - result = await this.terminalService.createTerminal({ config: launchConfigs }); - } + await this.terminalCreationQueue; + const createTerminalPromise = this.doCreateTerminal(group, launchConfigs); + this.terminalCreationQueue = createTerminalPromise; + const result: ITerminalInstance = await createTerminalPromise; const terminalKey = result.instanceId.toString(); result.onDisposed((terminal) => { diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts index fda5316ce13..d76761635fd 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts @@ -23,7 +23,7 @@ import { TerminalProtocolLinkProvider } from 'vs/workbench/contrib/terminal/brow import { TerminalValidatedLocalLinkProvider, lineAndColumnClause, unixLocalLinkClause, winLocalLinkClause, winDrivePrefix, winLineAndColumnMatchIndex, unixLineAndColumnMatchIndex, lineAndColumnClauseGroupCount } from 'vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider'; import { TerminalWordLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { XTermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; +import { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; import { TerminalHover, ILinkHoverTargetOptions } from 'vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget'; import { TerminalLink } from 'vs/workbench/contrib/terminal/browser/links/terminalLink'; import { TerminalExternalLinkProviderAdapter } from 'vs/workbench/contrib/terminal/browser/links/terminalExternalLinkProviderAdapter'; @@ -94,7 +94,7 @@ export class TerminalLinkManager extends DisposableStore { return; } - const core = (this._xterm as any)._core as XTermCore; + const core = (this._xterm as any)._core as IXtermCore; const cellDimensions = { width: core._renderService.dimensions.actualCellWidth, height: core._renderService.dimensions.actualCellHeight diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts index 46b346dc5aa..ad399d096e8 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts @@ -133,6 +133,9 @@ export class TerminalWordLinkProvider extends TerminalBaseLinkProvider { // that format link = normalize(link).replace(/^(\.+[\\/])+/, ''); + // Remove `:in` from the end which is how Ruby outputs stack traces + link = link.replace(/:in$/, ''); + // If any of the names of the folders in the workspace matches // a prefix of the link, remove that prefix and continue this._workspaceContextService.getWorkspace().folders.forEach((folder) => { diff --git a/src/vs/workbench/contrib/terminal/browser/remotePty.ts b/src/vs/workbench/contrib/terminal/browser/remotePty.ts index 1223a0d01d7..2a57bcab8e6 100644 --- a/src/vs/workbench/contrib/terminal/browser/remotePty.ts +++ b/src/vs/workbench/contrib/terminal/browser/remotePty.ts @@ -8,7 +8,7 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; -import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalLaunchError, IProcessProperty, IProcessPropertyMap, ProcessPropertyType, TerminalShellType, ProcessCapability } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, ITerminalChildProcess, ITerminalLaunchError, IProcessProperty, IProcessPropertyMap, ProcessPropertyType, ProcessCapability, IProcessReadyEvent } from 'vs/platform/terminal/common/terminal'; import { IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/terminalProcess'; import { RemoteTerminalChannelClient } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; @@ -16,22 +16,12 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA export class RemotePty extends Disposable implements ITerminalChildProcess { private readonly _onProcessData = this._register(new Emitter<string | IProcessDataEvent>()); readonly onProcessData = this._onProcessData.event; - private readonly _onProcessExit = this._register(new Emitter<number | undefined>()); - readonly onProcessExit = this._onProcessExit.event; private readonly _onProcessReady = this._register(new Emitter<IProcessReadyEvent>()); readonly onProcessReady = this._onProcessReady.event; - private readonly _onProcessTitleChanged = this._register(new Emitter<string>()); - readonly onProcessTitleChanged = this._onProcessTitleChanged.event; - private readonly _onProcessShellTypeChanged = this._register(new Emitter<TerminalShellType | undefined>()); - readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; - private readonly _onProcessOverrideDimensions = this._register(new Emitter<ITerminalDimensionsOverride | undefined>()); - readonly onProcessOverrideDimensions = this._onProcessOverrideDimensions.event; - private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter<IShellLaunchConfig>()); - readonly onProcessResolvedShellLaunchConfig = this._onProcessResolvedShellLaunchConfig.event; - private readonly _onDidChangeHasChildProcesses = this._register(new Emitter<boolean>()); - readonly onDidChangeHasChildProcesses = this._onDidChangeHasChildProcesses.event; private readonly _onDidChangeProperty = this._register(new Emitter<IProcessProperty<any>>()); readonly onDidChangeProperty = this._onDidChangeProperty.event; + private readonly _onProcessExit = this._register(new Emitter<number | undefined>()); + readonly onProcessExit = this._onProcessExit.event; private _startBarrier: Barrier; @@ -40,7 +30,12 @@ export class RemotePty extends Disposable implements ITerminalChildProcess { private _properties: IProcessPropertyMap = { cwd: '', initialCwd: '', - fixedDimensions: { cols: undefined, rows: undefined } + fixedDimensions: { cols: undefined, rows: undefined }, + title: '', + shellType: undefined, + hasChildProcesses: true, + resolvedShellLaunchConfig: {}, + overrideDimensions: undefined }; private _capabilities: ProcessCapability[] = []; @@ -145,42 +140,30 @@ export class RemotePty extends Disposable implements ITerminalChildProcess { handleData(e: string | IProcessDataEvent) { this._onProcessData.fire(e); } - processBinary(e: string): Promise<void> { - return this._remoteTerminalChannel.processBinary(this._id, e); - } handleExit(e: number | undefined) { this._onProcessExit.fire(e); } + processBinary(e: string): Promise<void> { + return this._remoteTerminalChannel.processBinary(this._id, e); + } handleReady(e: IProcessReadyEvent) { this._capabilities = e.capabilities; this._onProcessReady.fire(e); } - handleTitleChanged(e: string) { - this._onProcessTitleChanged.fire(e); - } - handleShellTypeChanged(e: TerminalShellType | undefined) { - this._onProcessShellTypeChanged.fire(e); - } - handleOverrideDimensions(e: ITerminalDimensionsOverride | undefined) { - this._onProcessOverrideDimensions.fire(e); - } - handleResolvedShellLaunchConfig(e: IShellLaunchConfig) { - // Revive the cwd URI - if (e.cwd && typeof e.cwd !== 'string') { - e.cwd = URI.revive(e.cwd); + handleDidChangeProperty({ type, value }: IProcessProperty<any>) { + switch (type) { + case ProcessPropertyType.Cwd: + this._properties.cwd = value; + break; + case ProcessPropertyType.InitialCwd: + this._properties.initialCwd = value; + break; + case ProcessPropertyType.ResolvedShellLaunchConfig: + if (value.cwd && typeof value.cwd !== 'string') { + value.cwd = URI.revive(value.cwd); + } } - this._onProcessResolvedShellLaunchConfig.fire(e); - } - handleDidChangeHasChildProcesses(e: boolean) { - this._onDidChangeHasChildProcesses.fire(e); - } - handleDidChangeProperty(e: IProcessProperty<any>) { - if (e.type === ProcessPropertyType.Cwd) { - this._properties.cwd = e.value; - } else if (e.type === ProcessPropertyType.InitialCwd) { - this._properties.initialCwd = e.value; - } - this._onDidChangeProperty.fire(e); + this._onDidChangeProperty.fire({ type, value }); } async handleReplay(e: IPtyHostProcessReplayEvent) { @@ -189,7 +172,7 @@ export class RemotePty extends Disposable implements ITerminalChildProcess { for (const innerEvent of e.events) { if (innerEvent.cols !== 0 || innerEvent.rows !== 0) { // never override with 0x0 as that is a marker for an unknown initial size - this._onProcessOverrideDimensions.fire({ cols: innerEvent.cols, rows: innerEvent.rows, forceExactSize: true }); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.OverrideDimensions, value: { cols: innerEvent.cols, rows: innerEvent.rows, forceExactSize: true } }); } const e: IProcessDataEvent = { data: innerEvent.data, trackCommit: true }; this._onProcessData.fire(e); @@ -200,7 +183,7 @@ export class RemotePty extends Disposable implements ITerminalChildProcess { } // remove size override - this._onProcessOverrideDimensions.fire(undefined); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.OverrideDimensions, value: undefined }); } handleOrphanQuestion() { diff --git a/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts b/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts index 3626519a4f0..0d3c39d687e 100644 --- a/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts @@ -17,7 +17,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { INotificationHandle, INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IRequestResolveVariablesEvent, IShellLaunchConfig, IShellLaunchConfigDto, ITerminalChildProcess, ITerminalProfile, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, ProcessPropertyType, TerminalIcon } from 'vs/platform/terminal/common/terminal'; +import { IRequestResolveVariablesEvent, IShellLaunchConfig, IShellLaunchConfigDto, ITerminalChildProcess, ITerminalProfile, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, ProcessPropertyType, TerminalIcon, TitleEventSource } from 'vs/platform/terminal/common/terminal'; import { IProcessDetails } from 'vs/platform/terminal/common/terminalProcess'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { RemotePty } from 'vs/workbench/contrib/terminal/browser/remotePty'; @@ -66,6 +66,11 @@ export class RemoteTerminalService extends Disposable implements IRemoteTerminal this._remoteTerminalChannel = channel; channel.onProcessData(e => this._ptys.get(e.id)?.handleData(e.event)); + channel.onProcessReplay(e => this._ptys.get(e.id)?.handleReplay(e.event)); + channel.onProcessOrphanQuestion(e => this._ptys.get(e.id)?.handleOrphanQuestion()); + channel.onDidRequestDetach(e => this._onDidRequestDetach.fire(e)); + channel.onProcessReady(e => this._ptys.get(e.id)?.handleReady(e.event)); + channel.onDidChangeProperty(e => this._ptys.get(e.id)?.handleDidChangeProperty(e.property)); channel.onProcessExit(e => { const pty = this._ptys.get(e.id); if (pty) { @@ -73,16 +78,6 @@ export class RemoteTerminalService extends Disposable implements IRemoteTerminal this._ptys.delete(e.id); } }); - channel.onProcessReady(e => this._ptys.get(e.id)?.handleReady(e.event)); - channel.onProcessTitleChanged(e => this._ptys.get(e.id)?.handleTitleChanged(e.event)); - channel.onProcessShellTypeChanged(e => this._ptys.get(e.id)?.handleShellTypeChanged(e.event)); - channel.onProcessOverrideDimensions(e => this._ptys.get(e.id)?.handleOverrideDimensions(e.event)); - channel.onProcessResolvedShellLaunchConfig(e => this._ptys.get(e.id)?.handleResolvedShellLaunchConfig(e.event)); - channel.onProcessReplay(e => this._ptys.get(e.id)?.handleReplay(e.event)); - channel.onProcessOrphanQuestion(e => this._ptys.get(e.id)?.handleOrphanQuestion()); - channel.onDidRequestDetach(e => this._onDidRequestDetach.fire(e)); - channel.onProcessDidChangeHasChildProcesses(e => this._ptys.get(e.id)?.handleDidChangeHasChildProcesses(e.event)); - channel.onDidChangeProperty(e => this._ptys.get(e.id)?.handleDidChangeProperty(e.property)); const allowedCommands = ['_remoteCLI.openExternal', '_remoteCLI.windowOpen', '_remoteCLI.getSystemStatus', '_remoteCLI.manageExtensions']; channel.onExecuteCommand(async e => { @@ -258,8 +253,8 @@ export class RemoteTerminalService extends Disposable implements IRemoteTerminal await this._remoteTerminalChannel?.updateProperty(id, property, value); } - async updateTitle(id: number, title: string): Promise<void> { - await this._remoteTerminalChannel?.updateTitle(id, title); + async updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise<void> { + await this._remoteTerminalChannel?.updateTitle(id, title, titleSource); } async updateIcon(id: number, icon: TerminalIcon, color?: string): Promise<void> { diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index 52d28939c1f..50650bbed36 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -17,7 +17,7 @@ import { getQuickNavigateHandler } from 'vs/workbench/browser/quickaccess'; import { Extensions as ViewContainerExtensions, IViewContainersRegistry, ViewContainerLocation, IViewsRegistry } from 'vs/workbench/common/views'; import { registerTerminalActions, terminalSendSequenceCommand } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; -import { TERMINAL_VIEW_ID, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TERMINAL_VIEW_ID, TerminalCommandId, ITerminalProfileService } from 'vs/workbench/contrib/terminal/common/terminal'; import { registerColors } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { setupTerminalCommands } from 'vs/workbench/contrib/terminal/browser/terminalCommands'; import { TerminalService } from 'vs/workbench/contrib/terminal/browser/terminalService'; @@ -45,6 +45,7 @@ import { TerminalEditorService } from 'vs/workbench/contrib/terminal/browser/ter import { TerminalInputSerializer } from 'vs/workbench/contrib/terminal/browser/terminalEditorSerializer'; import { TerminalGroupService } from 'vs/workbench/contrib/terminal/browser/terminalGroupService'; import { TerminalContextKeys, TerminalContextKeyStrings } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { TerminalProfileService } from 'vs/workbench/contrib/terminal/browser/terminalProfileService'; // Register services registerSingleton(ITerminalService, TerminalService, true); @@ -52,6 +53,7 @@ registerSingleton(ITerminalEditorService, TerminalEditorService, true); registerSingleton(ITerminalGroupService, TerminalGroupService, true); registerSingleton(IRemoteTerminalService, RemoteTerminalService); registerSingleton(ITerminalInstanceService, TerminalInstanceService, true); +registerSingleton(ITerminalProfileService, TerminalProfileService, true); // Register quick accesses const quickAccessRegistry = (Registry.as<IQuickAccessRegistry>(QuickAccessExtensions.Quickaccess)); diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index f161aeddf1a..063909586ad 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -8,12 +8,11 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, TerminalIcon, TitleEventSource, TerminalShellType, IExtensionTerminalProfile, TerminalLocation, ICreateContributedTerminalProfileOptions, ProcessPropertyType, ProcessCapability } from 'vs/platform/terminal/common/terminal'; -import { ICommandTracker, INavigationMode, IOffProcessTerminalService, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, TerminalIcon, TitleEventSource, TerminalShellType, IExtensionTerminalProfile, TerminalLocation, ProcessPropertyType, ProcessCapability } from 'vs/platform/terminal/common/terminal'; +import { ICommandTracker, INavigationMode, IOffProcessTerminalService, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalFont, ITerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/common/terminal'; import type { Terminal as XTermTerminal } from 'xterm'; import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; import type { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11'; -import type { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; import { ITerminalStatusList } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; import { ICompleteTerminalConfiguration } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; @@ -41,7 +40,6 @@ export interface ITerminalInstanceService { getXtermConstructor(): Promise<typeof XTermTerminal>; getXtermSearchConstructor(): Promise<typeof XTermSearchAddon>; getXtermUnicode11Constructor(): Promise<typeof XTermUnicode11Addon>; - getXtermWebglConstructor(): Promise<typeof XTermWebglAddon>; /** * Takes a path and returns the properly escaped path to send to the terminal. @@ -108,9 +106,6 @@ export interface ITerminalService extends ITerminalInstanceHost { configHelper: ITerminalConfigHelper; isProcessSupportRegistered: boolean; readonly connectionState: TerminalConnectionState; - readonly availableProfiles: ITerminalProfile[]; - readonly contributedProfiles: IExtensionTerminalProfile[]; - readonly profilesReady: Promise<void>; readonly defaultLocation: TerminalLocation; initializeTerminals(): Promise<void>; @@ -128,7 +123,6 @@ export interface ITerminalService extends ITerminalInstanceHost { onDidInputInstanceData: Event<ITerminalInstance>; onDidRegisterProcessSupport: Event<void>; onDidChangeConnectionState: Event<void>; - onDidChangeAvailableProfiles: Event<ITerminalProfile[]>; /** * Creates a terminal. @@ -171,8 +165,6 @@ export interface ITerminalService extends ITerminalInstanceHost { */ registerLinkProvider(linkProvider: ITerminalExternalLinkProvider): IDisposable; - registerTerminalProfileProvider(extensionIdenfifier: string, id: string, profileProvider: ITerminalProfileProvider): IDisposable; - showProfileQuickPick(type: 'setDefault' | 'createInstance', cwd?: string | URI): Promise<ITerminalInstance | undefined>; setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void; @@ -180,14 +172,14 @@ export interface ITerminalService extends ITerminalInstanceHost { requestStartExtensionTerminal(proxy: ITerminalProcessExtHostProxy, cols: number, rows: number): Promise<ITerminalLaunchError | undefined>; isAttachedToTerminal(remoteTerm: IRemoteTerminalAttachTarget): boolean; getEditableData(instance: ITerminalInstance): IEditableData | undefined; - setEditable(instance: ITerminalInstance, data: IEditableData | null): Promise<void>; + setEditable(instance: ITerminalInstance, data: IEditableData | null): void; + isEditable(instance: ITerminalInstance | undefined): boolean; safeDisposeTerminal(instance: ITerminalInstance): Promise<void>; getDefaultInstanceHost(): ITerminalInstanceHost; getInstanceHost(target: ITerminalLocationOptions | undefined): ITerminalInstanceHost; getFindHost(instance?: ITerminalInstance): ITerminalFindHost; - getDefaultProfileName(): string; resolveLocation(location?: ITerminalLocationOptions): TerminalLocation | undefined setNativeDelegate(nativeCalls: ITerminalServiceNativeDelegate): void; } @@ -347,10 +339,6 @@ export interface ITerminalExternalLinkProvider { provideLinks(instance: ITerminalInstance, line: string): Promise<ITerminalLink[] | undefined>; } -export interface ITerminalProfileProvider { - createContributedTerminalProfile(options: ICreateContributedTerminalProfileOptions): Promise<void>; -} - export interface ITerminalLink { /** The startIndex of the link in the line. */ startIndex: number; @@ -422,6 +410,9 @@ export interface ITerminalInstance { */ processId: number | undefined; + /** + * The position of the terminal. + */ target?: TerminalLocation; /** @@ -515,6 +506,11 @@ export interface ITerminalInstance { readonly areLinksReady: boolean; + /** + * The xterm.js instance for this terminal. + */ + readonly xterm?: IXtermTerminal; + /** * Returns an array of data events that have fired within the first 10 seconds. If this is * called 10 seconds after the terminal has existed the result will be undefined. This is useful @@ -568,12 +564,6 @@ export interface ITerminalInstance { */ disableLayout: boolean; - /** - * An object that tracks when commands are run and enables navigating and selecting between - * them. - */ - readonly commandTracker: ICommandTracker | undefined; - readonly navigationMode: INavigationMode | undefined; description: string | undefined; @@ -599,11 +589,6 @@ export interface ITerminalInstance { */ detachFromProcess(): Promise<void>; - /** - * Forces the terminal to redraw its viewport. - */ - forceRedraw(): void; - /** * Check if anything is selected in terminal. */ @@ -629,16 +614,6 @@ export interface ITerminalInstance { */ selectAll(): void; - /** - * Find the next instance of the term - */ - findNext(term: string, searchOptions: ISearchOptions): boolean; - - /** - * Find the previous instance of the term - */ - findPrevious(term: string, searchOptions: ISearchOptions): boolean; - /** * Notifies the terminal that the find widget's focus state has been changed. */ @@ -680,23 +655,18 @@ export interface ITerminalInstance { */ sendText(text: string, addNewLine: boolean): Promise<void>; - /** Scroll the terminal buffer down 1 line. */ - scrollDownLine(): void; - /** Scroll the terminal buffer down 1 page. */ - scrollDownPage(): void; - /** Scroll the terminal buffer to the bottom. */ - scrollToBottom(): void; - /** Scroll the terminal buffer up 1 line. */ - scrollUpLine(): void; - /** Scroll the terminal buffer up 1 page. */ - scrollUpPage(): void; - /** Scroll the terminal buffer to the top. */ - scrollToTop(): void; + /** Scroll the terminal buffer down 1 line. */ scrollDownLine(): void; + /** Scroll the terminal buffer down 1 page. */ scrollDownPage(): void; + /** Scroll the terminal buffer to the bottom. */ scrollToBottom(): void; + /** Scroll the terminal buffer up 1 line. */ scrollUpLine(): void; + /** Scroll the terminal buffer up 1 page. */ scrollUpPage(): void; + /** Scroll the terminal buffer to the top. */ scrollToTop(): void; /** - * Clears the terminal buffer, leaving only the prompt line. + * Clears the terminal buffer, leaving only the prompt line and moving it to the top of the + * viewport. */ - clear(): void; + clearBuffer(): void; /** * Attaches the terminal instance to an element on the DOM, before this is called the terminal @@ -792,6 +762,52 @@ export interface ITerminalInstance { changeColor(): Promise<void>; } +export interface IXtermTerminal { + /** + * An object that tracks when commands are run and enables navigating and selecting between + * them. + */ + readonly commandTracker: ICommandTracker; + + /** + * The position of the terminal. + */ + target?: TerminalLocation; + + /** + * Find the next instance of the term + */ + findNext(term: string, searchOptions: ISearchOptions): boolean; + + /** + * Find the previous instance of the term + */ + findPrevious(term: string, searchOptions: ISearchOptions): boolean; + + /** + * Forces the terminal to redraw its viewport. + */ + forceRedraw(): void; + + /** + * Gets the font metrics of this xterm.js instance. + */ + getFont(): ITerminalFont; + + /** Scroll the terminal buffer down 1 line. */ scrollDownLine(): void; + /** Scroll the terminal buffer down 1 page. */ scrollDownPage(): void; + /** Scroll the terminal buffer to the bottom. */ scrollToBottom(): void; + /** Scroll the terminal buffer up 1 line. */ scrollUpLine(): void; + /** Scroll the terminal buffer up 1 page. */ scrollUpPage(): void; + /** Scroll the terminal buffer to the top. */ scrollToTop(): void; + + /** + * Clears the terminal buffer, leaving only the prompt line and moving it to the top of the + * viewport. + */ + clearBuffer(): void; +} + export interface IRequestAddInstanceToGroupEvent { uri: URI; side: 'before' | 'after' diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index bf78f5916d7..1bacb7cd81c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -35,7 +35,7 @@ import { ResourceContextKey } from 'vs/workbench/common/resources'; import { FindInFilesCommand, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActions'; import { Direction, ICreateTerminalOptions, IRemoteTerminalService, ITerminalGroupService, ITerminalInstance, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalQuickAccessProvider } from 'vs/workbench/contrib/terminal/browser/terminalQuickAccess'; -import { ILocalTerminalService, IRemoteTerminalAttachTarget, ITerminalConfigHelper, TerminalCommandId, TERMINAL_ACTION_CATEGORY } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ILocalTerminalService, IRemoteTerminalAttachTarget, ITerminalConfigHelper, ITerminalProfileService, TerminalCommandId, TERMINAL_ACTION_CATEGORY } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { createProfileSchemaEnums } from 'vs/platform/terminal/common/terminalProfiles'; import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; @@ -880,9 +880,11 @@ export function registerTerminalActions() { return; } - await terminalService.setEditable(instance, { + terminalService.setEditable(instance, { validationMessage: value => validateTerminalName(value), onFinish: async (value, success) => { + // Cancel editing first as instance.rename will trigger a rerender automatically + terminalService.setEditable(instance, null); if (success) { try { await instance.rename(value); @@ -890,7 +892,6 @@ export function registerTerminalActions() { notificationService.error(e); } } - await terminalService.setEditable(instance, null); } }); } @@ -1033,7 +1034,7 @@ export function registerTerminalActions() { } run(accessor: ServicesAccessor) { accessor.get(ITerminalService).doWithActiveInstance(t => { - t.commandTracker?.scrollToPreviousCommand(); + t.xterm?.commandTracker.scrollToPreviousCommand(); t.focus(); }); } @@ -1055,7 +1056,7 @@ export function registerTerminalActions() { } run(accessor: ServicesAccessor) { accessor.get(ITerminalService).doWithActiveInstance(t => { - t.commandTracker?.scrollToNextCommand(); + t.xterm?.commandTracker.scrollToNextCommand(); t.focus(); }); } @@ -1077,7 +1078,7 @@ export function registerTerminalActions() { } run(accessor: ServicesAccessor) { accessor.get(ITerminalService).doWithActiveInstance(t => { - t.commandTracker?.selectToPreviousCommand(); + t.xterm?.commandTracker.selectToPreviousCommand(); t.focus(); }); } @@ -1099,7 +1100,7 @@ export function registerTerminalActions() { } run(accessor: ServicesAccessor) { accessor.get(ITerminalService).doWithActiveInstance(t => { - t.commandTracker?.selectToNextCommand(); + t.xterm?.commandTracker.selectToNextCommand(); t.focus(); }); } @@ -1116,7 +1117,7 @@ export function registerTerminalActions() { } run(accessor: ServicesAccessor) { accessor.get(ITerminalService).doWithActiveInstance(t => { - t.commandTracker?.selectToPreviousLine(); + t.xterm?.commandTracker.selectToPreviousLine(); t.focus(); }); } @@ -1133,7 +1134,7 @@ export function registerTerminalActions() { } run(accessor: ServicesAccessor) { accessor.get(ITerminalService).doWithActiveInstance(t => { - t.commandTracker?.selectToNextLine(); + t.xterm?.commandTracker.selectToNextLine(); t.focus(); }); } @@ -1802,7 +1803,7 @@ export function registerTerminalActions() { }); } run(accessor: ServicesAccessor) { - accessor.get(ITerminalService).doWithActiveInstance(t => t.clear()); + accessor.get(ITerminalService).doWithActiveInstance(t => t.clearBuffer()); } }); registerAction2(class extends Action2 { @@ -1981,6 +1982,7 @@ export function registerTerminalActions() { } async run(accessor: ServicesAccessor, item?: string) { const terminalService = accessor.get(ITerminalService); + const terminalProfileService = accessor.get(ITerminalProfileService); const terminalGroupService = accessor.get(ITerminalGroupService); if (!item || !item.split) { return Promise.resolve(null); @@ -1999,7 +2001,7 @@ export function registerTerminalActions() { return terminalGroupService.showPanel(true); } - const quickSelectProfiles = terminalService.availableProfiles; + const quickSelectProfiles = terminalProfileService.availableProfiles; // Remove 'New ' from the selected item to get the profile name const profileSelection = item.substring(4); @@ -2108,6 +2110,7 @@ export function refreshTerminalActions(detectedProfiles: ITerminalProfile[]) { } async run(accessor: ServicesAccessor, eventOrOptionsOrProfile: MouseEvent | ICreateTerminalOptions | ITerminalProfile | { profileName: string } | undefined, profile?: ITerminalProfile) { const terminalService = accessor.get(ITerminalService); + const terminalProfileService = accessor.get(ITerminalProfileService); if (!terminalService.isProcessSupportRegistered) { return; @@ -2123,7 +2126,7 @@ export function refreshTerminalActions(detectedProfiles: ITerminalProfile[]) { let cwd: string | URI | undefined; if (typeof eventOrOptionsOrProfile === 'object' && eventOrOptionsOrProfile && 'profileName' in eventOrOptionsOrProfile) { - const config = terminalService.availableProfiles.find(profile => profile.profileName === eventOrOptionsOrProfile.profileName); + const config = terminalProfileService.availableProfiles.find(profile => profile.profileName === eventOrOptionsOrProfile.profileName); if (!config) { throw new Error(`Could not find terminal profile "${eventOrOptionsOrProfile.profileName}"`); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts index 8ad20f4f43d..dff4154376e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts @@ -17,7 +17,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { InstallRecommendedExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IProductService } from 'vs/platform/product/common/productService'; -import { XTermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; +import { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; import { IShellLaunchConfig } from 'vs/platform/terminal/common/terminal'; import { isLinux, isWindows } from 'vs/base/common/platform'; @@ -154,7 +154,7 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { * Gets the font information based on the terminal.integrated.fontFamily * terminal.integrated.fontSize, terminal.integrated.lineHeight configuration properties */ - getFont(xtermCore?: XTermCore, excludeDimensions?: boolean): ITerminalFont { + getFont(xtermCore?: IXtermCore, excludeDimensions?: boolean): ITerminalFont { const editorConfig = this._configurationService.getValue<IEditorOptions>('editor'); let fontFamily = this.config.fontFamily || editorConfig.fontFamily || EDITOR_FONT_DEFAULTS.fontFamily; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index e95ff7bcb4d..124238bcac6 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts @@ -23,7 +23,7 @@ import { ITerminalEditorService, ITerminalService } from 'vs/workbench/contrib/t import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; import { TerminalFindWidget } from 'vs/workbench/contrib/terminal/browser/terminalFindWidget'; import { getTerminalActionBarArgs } from 'vs/workbench/contrib/terminal/browser/terminalMenus'; -import { ITerminalProfileResolverService, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalProfileResolverService, ITerminalProfileService, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { isLinux, isMacintosh } from 'vs/base/common/platform'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; @@ -69,7 +69,8 @@ export class TerminalEditor extends EditorPane { @IMenuService menuService: IMenuService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IContextMenuService private readonly _contextMenuService: IContextMenuService, - @INotificationService private readonly _notificationService: INotificationService + @INotificationService private readonly _notificationService: INotificationService, + @ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService ) { super(TerminalEditor.ID, telemetryService, themeService, storageService); this._findState = new FindReplaceState(); @@ -201,7 +202,7 @@ export class TerminalEditor extends EditorPane { switch (action.id) { case TerminalCommandId.CreateWithProfileButton: { const location = { viewColumn: ACTIVE_GROUP }; - const actions = getTerminalActionBarArgs(location, this._terminalService.availableProfiles, this._getDefaultProfileName(), this._terminalService.contributedProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); + const actions = getTerminalActionBarArgs(location, this._terminalProfileService.availableProfiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); const button = this._instantiationService.createInstance(DropdownWithPrimaryActionViewItem, actions.primaryAction, actions.dropdownAction, actions.dropdownMenuActions, actions.className, this._contextMenuService, {}); return button; } @@ -212,7 +213,7 @@ export class TerminalEditor extends EditorPane { private _getDefaultProfileName(): string { let defaultProfileName; try { - defaultProfileName = this._terminalService.getDefaultProfileName(); + defaultProfileName = this._terminalProfileService.getDefaultProfileName(); } catch (e) { defaultProfileName = this._terminalProfileResolverService.defaultProfileName; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts index c82e6a51b33..cc17951bb90 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts @@ -36,9 +36,9 @@ export class TerminalFindWidget extends SimpleFindWidget { return; } if (previous) { - instance.findPrevious(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }); + instance.xterm?.findPrevious(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }); } else { - instance.findNext(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }); + instance.xterm?.findNext(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }); } } override reveal(initialInput?: string): void { @@ -63,8 +63,8 @@ export class TerminalFindWidget extends SimpleFindWidget { protected _onInputChanged() { // Ignore input changes for now const instance = this._terminalService.activeInstance; - if (instance) { - return instance.findPrevious(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue(), incremental: true }); + if (instance?.xterm) { + return instance.xterm.findPrevious(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue(), incremental: true }); } return false; } @@ -99,7 +99,7 @@ export class TerminalFindWidget extends SimpleFindWidget { if (instance.hasSelection()) { instance.clearSelection(); } - instance.findPrevious(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }); + instance.xterm?.findPrevious(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }); } } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index d67d10b9ab5..7f715cde554 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -13,32 +13,24 @@ import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/l import { TabFocus } from 'vs/editor/common/config/commonEditorConfig'; import * as nls from 'vs/nls'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILogService } from 'vs/platform/log/common/log'; -import { INotificationService, IPromptChoice, NeverShowAgainScope, Severity } from 'vs/platform/notification/common/notification'; +import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { activeContrastBorder, editorBackground, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground } from 'vs/platform/theme/common/colorRegistry'; +import { activeContrastBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground } from 'vs/platform/theme/common/colorRegistry'; import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; import { ITerminalProcessManager, ProcessState, TERMINAL_VIEW_ID, INavigationMode, DEFAULT_COMMANDS_TO_SKIP_SHELL, TERMINAL_CREATION_COMMANDS, ITerminalProfileResolverService, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; -import { ansiColorIdentifiers, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_BACKGROUND_COLOR, TERMINAL_CURSOR_FOREGROUND_COLOR, TERMINAL_FOREGROUND_COLOR, TERMINAL_SELECTION_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { ITerminalInstanceService, ITerminalInstance, ITerminalExternalLinkProvider, IRequestAddInstanceToGroupEvent } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalProcessManager } from 'vs/workbench/contrib/terminal/browser/terminalProcessManager'; -import type { Terminal as XTermTerminal, IBuffer, ITerminalAddon, RendererType, ITheme } from 'xterm'; -import type { SearchAddon, ISearchOptions } from 'xterm-addon-search'; -import type { Unicode11Addon } from 'xterm-addon-unicode11'; -import type { WebglAddon } from 'xterm-addon-webgl'; -import { CommandTrackerAddon } from 'vs/workbench/contrib/terminal/browser/addons/commandTrackerAddon'; -import { NavigationModeAddon } from 'vs/workbench/contrib/terminal/browser/addons/navigationModeAddon'; -import { XTermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; -import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import type { Terminal as XTermTerminal, IBuffer, ITerminalAddon } from 'xterm'; +import { NavigationModeAddon } from 'vs/workbench/contrib/terminal/browser/xterm/navigationModeAddon'; import { IViewsService, IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; import { EnvironmentVariableInfoWidget } from 'vs/workbench/contrib/terminal/browser/widgets/environmentVariableInfoWidget'; import { TerminalLaunchHelpAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; @@ -46,7 +38,7 @@ import { TypeAheadAddon } from 'vs/workbench/contrib/terminal/browser/terminalTy import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, TerminalShellType, TerminalSettingId, TitleEventSource, TerminalIcon, TerminalSettingPrefix, ITerminalProfileObject, TerminalLocation, ProcessPropertyType, ProcessCapability, IProcessPropertyMap } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, TerminalShellType, TerminalSettingId, TitleEventSource, TerminalIcon, TerminalLocation, ProcessPropertyType, ProcessCapability, IProcessPropertyMap } from 'vs/platform/terminal/common/terminal'; import { IProductService } from 'vs/platform/product/common/productService'; import { formatMessageForTerminal } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { AutoOpenBarrier, Promises } from 'vs/base/common/async'; @@ -61,28 +53,17 @@ import { CodeDataTransfers, containsDragType, DragAndDropObserver, IDragAndDropO import { getColorClass, getColorStyleElement, getStandardColors } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; import { IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { Orientation } from 'vs/base/browser/ui/sash/sash'; -import { Color } from 'vs/base/common/color'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { getTerminalResourcesFromDragEvent, getTerminalUri } from 'vs/workbench/contrib/terminal/browser/terminalUri'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; -import { isSafari } from 'vs/base/browser/browser'; import { ISeparator, template } from 'vs/base/common/labels'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; -import { LineDataEventAddon } from 'vs/workbench/contrib/terminal/browser/addons/lineDataEventAddon'; - -// How long in milliseconds should an average frame take to render for a notification to appear -// which suggests the fallback DOM-based renderer -const SLOW_CANVAS_RENDER_THRESHOLD = 50; -const NUMBER_OF_FRAMES_TO_MEASURE = 20; - -const SHOULD_PROMPT_FOR_PROFILE_MIGRATION_KEY = 'terminals.integrated.profile-migration'; - -let migrationMessageShown = false; +import { LineDataEventAddon } from 'vs/workbench/contrib/terminal/browser/xterm/lineDataEventAddon'; +import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; const enum Constants { /** @@ -116,7 +97,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private static _lastKnownCanvasDimensions: ICanvasDimensions | undefined; private static _lastKnownGridDimensions: IGridDimensions | undefined; private static _instanceIdCounter = 1; - private static _suggestedRendererType: 'canvas' | 'dom' | undefined = undefined; + + xterm?: XtermTerminal; + private _xtermReadyPromise: Promise<XtermTerminal>; + private _xtermTypeAheadAddon: TypeAheadAddon | undefined; private _processManager!: ITerminalProcessManager; private _pressAnyKeyToCloseListener: IDisposable | undefined; @@ -135,12 +119,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _titleSource: TitleEventSource = TitleEventSource.Process; private _container: HTMLElement | undefined; private _wrapperElement: (HTMLElement & { xterm?: XTermTerminal }) | undefined; - private _xterm: XTermTerminal | undefined; - private _xtermCore: XTermCore | undefined; - private _xtermTypeAhead: TypeAheadAddon | undefined; - private _xtermSearch: SearchAddon | undefined; - private _xtermUnicode11: Unicode11Addon | undefined; - private _xtermElement: HTMLDivElement | undefined; private _horizontalScrollbar: DomScrollableElement | undefined; private _terminalHasTextContextKey: IContextKey<boolean>; private _terminalA11yTreeFocusContextKey: IContextKey<boolean>; @@ -151,7 +129,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _cwd: string | undefined = undefined; private _initialCwd: string | undefined = undefined; private _dimensionsOverride: ITerminalDimensionsOverride | undefined; - private _xtermReadyPromise: Promise<XTermTerminal>; private _titleReadyPromise: Promise<string>; private _titleReadyComplete: ((title: string) => any) | undefined; private _areLinksReady: boolean = false; @@ -164,8 +141,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _widgetManager: TerminalWidgetManager = this._instantiationService.createInstance(TerminalWidgetManager); private _linkManager: TerminalLinkManager | undefined; private _environmentInfo: { widget: EnvironmentVariableInfoWidget, disposable: IDisposable } | undefined; - private _webglAddon: WebglAddon | undefined; - private _commandTrackerAddon: CommandTrackerAddon | undefined; private _navigationModeAddon: INavigationMode & ITerminalAddon | undefined; private _dndObserver: IDisposable | undefined; @@ -175,7 +150,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _hasHadInput: boolean; - readonly statusList: ITerminalStatusList; disableLayout: boolean = false; @@ -189,7 +163,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _userHome?: string; private _hasScrollBar?: boolean; - target?: TerminalLocation; + get target(): TerminalLocation | undefined { return this.xterm?.target; } + set target(value: TerminalLocation | undefined) { + if (this.xterm) { + this.xterm.target = value; + } + } + get instanceId(): number { return this._instanceId; } get resource(): URI { return this._resource; } get cols(): number { @@ -234,7 +214,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { get isTitleSetByProcess(): boolean { return !!this._messageTitleDisposable; } get shellLaunchConfig(): IShellLaunchConfig { return this._shellLaunchConfig; } get shellType(): TerminalShellType { return this._shellType; } - get commandTracker(): CommandTrackerAddon | undefined { return this._commandTrackerAddon; } get navigationMode(): INavigationMode | undefined { return this._navigationModeAddon; } get isDisconnected(): boolean { return this._processManager.isDisconnected; } get isRemote(): boolean { return this._processManager.remoteAuthority !== undefined; } @@ -314,7 +293,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { @ILogService private readonly _logService: ILogService, @IStorageService private readonly _storageService: IStorageService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, - @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService, @IProductService private readonly _productService: IProductService, @IQuickInputService private readonly _quickInputService: IQuickInputService, @IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService, @@ -385,10 +363,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { }); this.addDisposable(this._configurationService.onDidChangeConfiguration(async e => { - if (e.affectsConfiguration(TerminalSettingId.GpuAcceleration)) { - TerminalInstance._suggestedRendererType = undefined; - } - if (e.affectsConfiguration('terminal.integrated') || e.affectsConfiguration('editor.fastScrollSensitivity') || e.affectsConfiguration('editor.mouseWheelScrollSensitivity') || e.affectsConfiguration('editor.multiCursorModifier')) { + if (e.affectsConfiguration('terminal.integrated')) { this.updateConfig(); this.setVisible(this._isVisible); } @@ -430,7 +405,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { window.clearTimeout(initialDataEventsTimeout); } })); - this.showProfileMigrationNotification(); } private _getIcon(): TerminalIcon | undefined { @@ -458,53 +432,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._register(disposable); } - async showProfileMigrationNotification(): Promise<void> { - const platform = this._getPlatformKey(); - const shouldMigrateToProfile = (!!this._configurationService.getValue(TerminalSettingPrefix.Shell + platform) || - !!this._configurationService.inspect(TerminalSettingPrefix.ShellArgs + platform).userValue) && - !!this._configurationService.getValue(TerminalSettingPrefix.DefaultProfile + platform); - if (shouldMigrateToProfile && this._storageService.getBoolean(SHOULD_PROMPT_FOR_PROFILE_MIGRATION_KEY, StorageScope.WORKSPACE, true) && !migrationMessageShown) { - this._notificationService.prompt( - Severity.Info, - nls.localize('terminalProfileMigration', "The terminal is using deprecated shell/shellArgs settings, do you want to migrate it to a profile?"), - [ - { - label: nls.localize('migrateToProfile', "Migrate"), - run: async () => { - const shell = this._configurationService.getValue(TerminalSettingPrefix.Shell + platform); - const shellArgs = this._configurationService.getValue(TerminalSettingPrefix.ShellArgs + platform); - const profile = await this._terminalProfileResolverService.createProfileFromShellAndShellArgs(shell, shellArgs); - if (typeof profile === 'string') { - await this._configurationService.updateValue(TerminalSettingPrefix.DefaultProfile + platform, profile); - this._logService.trace(`migrated from shell/shellArgs, using existing profile ${profile}`); - } else { - const profiles = { ...this._configurationService.inspect<Readonly<{ [key: string]: ITerminalProfileObject }>>(TerminalSettingPrefix.Profiles + platform).userValue } || {}; - const profileConfig: ITerminalProfileObject = { path: profile.path }; - if (profile.args) { - profileConfig.args = profile.args; - } - profiles[profile.profileName] = profileConfig; - await this._configurationService.updateValue(TerminalSettingPrefix.Profiles + platform, profiles); - await this._configurationService.updateValue(TerminalSettingPrefix.DefaultProfile + platform, profile.profileName); - this._logService.trace(`migrated from shell/shellArgs, ${shell} ${shellArgs} to profile ${JSON.stringify(profile)}`); - } - await this._configurationService.updateValue(TerminalSettingPrefix.Shell + platform, undefined); - await this._configurationService.updateValue(TerminalSettingPrefix.ShellArgs + platform, undefined); - } - } as IPromptChoice, - ], - { - neverShowAgain: { id: SHOULD_PROMPT_FOR_PROFILE_MIGRATION_KEY, scope: NeverShowAgainScope.WORKSPACE } - } - ); - migrationMessageShown = true; - } - } - - private _getPlatformKey(): string { - return isWindows ? 'windows' : (isMacintosh ? 'osx' : 'linux'); - } - private _initDimensions(): void { // The terminal panel needs to have been created if (!this._container) { @@ -536,7 +463,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return null; } - const font = this._configHelper.getFont(this._xtermCore); + const font = this.xterm ? this.xterm.getFont() : this._configHelper.getFont(); if (!font.charWidth || !font.charHeight) { this._setLastKnownColsAndRows(); return null; @@ -579,7 +506,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _getDimension(width: number, height: number): ICanvasDimensions | undefined { // The font needs to have been initialized - const font = this._configHelper.getFont(this._xtermCore); + const font = this.xterm ? this.xterm.getFont() : this._configHelper.getFont(); if (!font || !font.charWidth || !font.charHeight) { return undefined; } @@ -611,53 +538,19 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { /** * Create xterm.js instance and attach data listeners. */ - protected async _createXterm(): Promise<XTermTerminal> { + protected async _createXterm(): Promise<XtermTerminal> { const Terminal = await this._getXtermConstructor(); - const font = this._configHelper.getFont(undefined, true); - const config = this._configHelper.config; - const editorOptions = this._configurationService.getValue<IEditorOptions>('editor'); - const xterm = new Terminal({ - cols: this._cols || Constants.DefaultCols, - rows: this._rows || Constants.DefaultRows, - altClickMovesCursor: config.altClickMovesCursor && editorOptions.multiCursorModifier === 'alt', - scrollback: config.scrollback, - theme: this._getXtermTheme(), - drawBoldTextInBrightColors: config.drawBoldTextInBrightColors, - fontFamily: font.fontFamily, - fontWeight: config.fontWeight, - fontWeightBold: config.fontWeightBold, - fontSize: font.fontSize, - letterSpacing: font.letterSpacing, - lineHeight: font.lineHeight, - minimumContrastRatio: config.minimumContrastRatio, - cursorBlink: config.cursorBlinking, - cursorStyle: config.cursorStyle === 'line' ? 'bar' : config.cursorStyle, - cursorWidth: config.cursorWidth, - bellStyle: 'none', - macOptionIsMeta: config.macOptionIsMeta, - macOptionClickForcesSelection: config.macOptionClickForcesSelection, - rightClickSelectsWord: config.rightClickBehavior === 'selectWord', - fastScrollModifier: 'alt', - fastScrollSensitivity: editorOptions.fastScrollSensitivity, - scrollSensitivity: editorOptions.mouseWheelScrollSensitivity, - rendererType: this._getBuiltInXtermRenderer(config.gpuAcceleration, TerminalInstance._suggestedRendererType), - wordSeparator: config.wordSeparators - }); - this._xterm = xterm; - this._xtermCore = (xterm as any)._core as XTermCore; + // TODO: Move cols/rows over to XtermTerminal + const xterm = this._instantiationService.createInstance(XtermTerminal, Terminal, this._configHelper, this._cols, this._rows); + this.xterm = xterm; const lineDataEventAddon = new LineDataEventAddon(); - this._xterm.loadAddon(lineDataEventAddon); - this._updateUnicodeVersion(); + this.xterm.raw.loadAddon(lineDataEventAddon); this.updateAccessibilitySupport(); - this._terminalInstanceService.getXtermSearchConstructor().then(addonCtor => { - this._xtermSearch = new addonCtor(); - xterm.loadAddon(this._xtermSearch); - }); // Write initial text, deferring onLineFeed listener when applicable to avoid firing // onLineData events containing initialText if (this._shellLaunchConfig.initialText) { - this._xterm.writeln(this._shellLaunchConfig.initialText, () => { + this.xterm.raw.writeln(this._shellLaunchConfig.initialText, () => { lineDataEventAddon.onLineData(e => this._onLineData.fire(e)); }); } else { @@ -666,7 +559,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Delay the creation of the bell listener to avoid showing the bell when the terminal // starts up or reconnects setTimeout(() => { - this._xterm?.onBell(() => { + xterm.raw.onBell(() => { if (this._configHelper.config.enableBell) { this.statusList.add({ id: TerminalStatus.Bell, @@ -677,16 +570,16 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } }); }, 1000); - this._xterm.onKey(e => this._onKey(e.key, e.domEvent)); - this._xterm.onSelectionChange(async () => this._onSelectionChange()); - this._xterm.buffer.onBufferChange(() => this._refreshAltBufferContextKey()); + xterm.raw.onKey(e => this._onKey(e.key, e.domEvent)); + xterm.raw.onSelectionChange(async () => this._onSelectionChange()); + xterm.raw.buffer.onBufferChange(() => this._refreshAltBufferContextKey()); this._processManager.onProcessData(e => this._onProcessData(e)); - this._xterm.onData(async data => { + xterm.raw.onData(async data => { await this._processManager.write(data); this._onDidInputData.fire(this); }); - this._xterm.onBinary(data => this._processManager.processBinary(data)); + xterm.raw.onBinary(data => this._processManager.processBinary(data)); this.processReady.then(async () => { if (this._linkManager) { this._linkManager.processCwd = await this._processManager.getInitialCwd(); @@ -704,24 +597,16 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { lineDataEventAddon.setOperatingSystem(this._processManager.os); } if (this._processManager.os === OperatingSystem.Windows) { - xterm.setOption('windowsMode', processTraits.requiresWindowsMode || false); + xterm.raw.setOption('windowsMode', processTraits.requiresWindowsMode || false); } - this._linkManager = this._instantiationService.createInstance(TerminalLinkManager, xterm, this._processManager!); + this._linkManager = this._instantiationService.createInstance(TerminalLinkManager, xterm.raw, this._processManager!); this._areLinksReady = true; this._onLinksReady.fire(this); }); - this._commandTrackerAddon = new CommandTrackerAddon(); - this._xterm.loadAddon(this._commandTrackerAddon); - this._register(this._themeService.onDidColorThemeChange(theme => this._updateTheme(xterm, theme))); - this._register(this._viewDescriptorService.onDidChangeLocation(({ views }) => { - if (views.some(v => v.id === TERMINAL_VIEW_ID)) { - this._updateTheme(xterm); - } - })); - - this._xtermTypeAhead = this._register(this._instantiationService.createInstance(TypeAheadAddon, this._processManager, this._configHelper)); - this._xterm.loadAddon(this._xtermTypeAhead); + // TODO: This should be an optional addon + this._xtermTypeAheadAddon = this._register(this._instantiationService.createInstance(TypeAheadAddon, this._processManager, this._configHelper)); + xterm.raw.loadAddon(this._xtermTypeAheadAddon); this._pathService.userHome().then(userHome => { this._userHome = userHome.fsPath; }); @@ -742,16 +627,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._attachBarrier.open(); + this.xterm?.attachToElement(container); + // Attach has not occurred yet if (!this._wrapperElement) { return this._attachToElement(container); } - // Update the theme when attaching as the terminal location could have changed - if (this._xterm) { - this._updateTheme(this._xterm); - } - // The container changed, reattach this._container = container; this._container.appendChild(this._wrapperElement); @@ -766,27 +648,26 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._container = container; this._wrapperElement = document.createElement('div'); this._wrapperElement.classList.add('terminal-wrapper'); - this._xtermElement = document.createElement('div'); - this._wrapperElement.appendChild(this._xtermElement); + const xtermElement = document.createElement('div'); + this._wrapperElement.appendChild(xtermElement); this._container.appendChild(this._wrapperElement); const xterm = await this._xtermReadyPromise; // Attach the xterm object to the DOM, exposing it to the smoke tests - this._wrapperElement.xterm = xterm; + this._wrapperElement.xterm = xterm.raw; - this._updateTheme(xterm); - xterm.open(this._xtermElement); + xterm.attachToElement(xtermElement); - if (!xterm.element || !xterm.textarea) { + if (!xterm.raw.element || !xterm.raw.textarea) { throw new Error('xterm elements not set after open'); } - this._setAriaLabel(xterm, this._instanceId, this._title); + this._setAriaLabel(xterm.raw, this._instanceId, this._title); - xterm.attachCustomKeyEventHandler((event: KeyboardEvent): boolean => { + xterm.raw.attachCustomKeyEventHandler((event: KeyboardEvent): boolean => { // Disable all input if the terminal is exiting if (this._isExiting) { return false; @@ -865,7 +746,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return true; }); - this._register(dom.addDisposableListener(xterm.element, 'mousedown', () => { + this._register(dom.addDisposableListener(xterm.raw.element, 'mousedown', () => { // We need to listen to the mouseup event on the document since the user may release // the mouse button anywhere outside of _xterm.element. const listener = dom.addDisposableListener(document, 'mouseup', () => { @@ -875,11 +756,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { listener.dispose(); }); })); - this._register(dom.addDisposableListener(xterm.element, 'touchstart', () => { - xterm.focus(); + this._register(dom.addDisposableListener(xterm.raw.element, 'touchstart', () => { + xterm.raw.focus(); })); - this._register(dom.addDisposableListener(xterm.element, 'wheel', (e) => { + this._register(dom.addDisposableListener(xterm.raw.element, 'wheel', (e) => { if (this._hasScrollBar && e.shiftKey) { e.stopImmediatePropagation(); e.preventDefault(); @@ -887,13 +768,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { })); // xterm.js currently drops selection on keyup as we need to handle this case. - this._register(dom.addDisposableListener(xterm.element, 'keyup', () => { + this._register(dom.addDisposableListener(xterm.raw.element, 'keyup', () => { // Wait until keyup has propagated through the DOM before evaluating // the new selection state. setTimeout(() => this._refreshSelectionContextKey(), 0); })); - this._register(dom.addDisposableListener(xterm.textarea, 'focus', () => { + this._register(dom.addDisposableListener(xterm.raw.textarea, 'focus', () => { this._terminalFocusContextKey.set(true); if (this.shellType) { this._terminalShellTypeContextKey.set(this.shellType.toString()); @@ -903,7 +784,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._onDidFocus.fire(this); })); - this._register(dom.addDisposableListener(xterm.textarea, 'blur', () => { + this._register(dom.addDisposableListener(xterm.raw.textarea, 'blur', () => { this._terminalFocusContextKey.reset(); this._onDidBlur.fire(this); this._refreshSelectionContextKey(); @@ -911,7 +792,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._initDragAndDrop(container); - this._widgetManager.attachToElement(xterm.element); + this._widgetManager.attachToElement(xterm.raw.element); this._processManager.onProcessReady((e) => { this._linkManager?.setWidgetManager(this._widgetManager); this._capabilities = e.capabilities; @@ -930,8 +811,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // If IShellLaunchConfig.waitOnExit was true and the process finished before the terminal // panel was initialized. - if (xterm.getOption('disableStdin')) { - this._attachPressAnyKeyToCloseListener(xterm); + if (xterm.raw.getOption('disableStdin')) { + this._attachPressAnyKeyToCloseListener(xterm.raw); } } @@ -947,122 +828,52 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._dndObserver = new DragAndDropObserver(container, dndController); } - private async _measureRenderTime(): Promise<void> { - await this._xtermReadyPromise; - const frameTimes: number[] = []; - if (!this._xtermCore?._renderService) { - return; - } - const textRenderLayer = this._xtermCore!._renderService?._renderer._renderLayers[0]; - const originalOnGridChanged = textRenderLayer?.onGridChanged; - const evaluateCanvasRenderer = () => { - // Discard first frame time as it's normal to take longer - frameTimes.shift(); - - const medianTime = frameTimes.sort((a, b) => a - b)[Math.floor(frameTimes.length / 2)]; - if (medianTime > SLOW_CANVAS_RENDER_THRESHOLD) { - if (this._configHelper.config.gpuAcceleration === 'auto') { - TerminalInstance._suggestedRendererType = 'dom'; - this.updateConfig(); - } else { - const promptChoices: IPromptChoice[] = [ - { - label: nls.localize('yes', "Yes"), - run: () => this._configurationService.updateValue(TerminalSettingId.GpuAcceleration, 'off', ConfigurationTarget.USER) - } as IPromptChoice, - { - label: nls.localize('no', "No"), - run: () => { } - } as IPromptChoice, - { - label: nls.localize('dontShowAgain', "Don't Show Again"), - isSecondary: true, - run: () => this._storageService.store(TerminalStorageKeys.NeverMeasureRenderTime, true, StorageScope.GLOBAL, StorageTarget.MACHINE) - } as IPromptChoice - ]; - this._notificationService.prompt( - Severity.Warning, - nls.localize('terminal.slowRendering', 'Terminal GPU acceleration appears to be slow on your computer. Would you like to switch to disable it which may improve performance? [Read more about terminal settings](https://code.visualstudio.com/docs/editor/integrated-terminal#_changing-how-the-terminal-is-rendered).'), - promptChoices - ); - } - } - }; - - textRenderLayer.onGridChanged = (terminal: XTermTerminal, firstRow: number, lastRow: number) => { - const startTime = performance.now(); - originalOnGridChanged.call(textRenderLayer, terminal, firstRow, lastRow); - frameTimes.push(performance.now() - startTime); - if (frameTimes.length === NUMBER_OF_FRAMES_TO_MEASURE) { - evaluateCanvasRenderer(); - // Restore original function - textRenderLayer.onGridChanged = originalOnGridChanged; - } - }; - } - hasSelection(): boolean { - return this._xterm ? this._xterm.hasSelection() : false; + return this.xterm ? this.xterm.raw.hasSelection() : false; } async copySelection(): Promise<void> { const xterm = await this._xtermReadyPromise; if (this.hasSelection()) { - await this._clipboardService.writeText(xterm.getSelection()); + await this._clipboardService.writeText(xterm.raw.getSelection()); } else { this._notificationService.warn(nls.localize('terminal.integrated.copySelection.noSelection', 'The terminal has no selection to copy')); } } get selection(): string | undefined { - return this._xterm && this.hasSelection() ? this._xterm.getSelection() : undefined; + return this.xterm && this.hasSelection() ? this.xterm.raw.getSelection() : undefined; } clearSelection(): void { - this._xterm?.clearSelection(); + this.xterm?.raw.clearSelection(); } selectAll(): void { // Focus here to ensure the terminal context key is set - this._xterm?.focus(); - this._xterm?.selectAll(); - } - - findNext(term: string, searchOptions: ISearchOptions): boolean { - if (!this._xtermSearch) { - return false; - } - return this._xtermSearch.findNext(term, searchOptions); - } - - findPrevious(term: string, searchOptions: ISearchOptions): boolean { - if (!this._xtermSearch) { - return false; - } - return this._xtermSearch.findPrevious(term, searchOptions); + this.xterm?.raw.focus(); + this.xterm?.raw.selectAll(); } notifyFindWidgetFocusChanged(isFocused: boolean): void { - if (!this._xterm) { + if (!this.xterm) { return; } - const terminalFocused = !isFocused && (document.activeElement === this._xterm.textarea || document.activeElement === this._xterm.element); + const terminalFocused = !isFocused && (document.activeElement === this.xterm.raw.textarea || document.activeElement === this.xterm.raw.element); this._terminalFocusContextKey.set(terminalFocused); } private _refreshAltBufferContextKey() { - this._terminalAltBufferActiveContextKey.set(!!(this._xterm && this._xterm.buffer.active === this._xterm.buffer.alternate)); + this._terminalAltBufferActiveContextKey.set(!!(this.xterm && this.xterm.raw.buffer.active === this.xterm.raw.buffer.alternate)); } override dispose(immediate?: boolean): void { this._logService.trace(`terminalInstance#dispose (instanceId: ${this.instanceId})`); dispose(this._linkManager); this._linkManager = undefined; - dispose(this._commandTrackerAddon); - this._commandTrackerAddon = undefined; dispose(this._widgetManager); - if (this._xterm && this._xterm.element) { + if (this.xterm?.raw.element) { this._hadFocusOnExit = this.hasFocus; } if (this._wrapperElement) { @@ -1074,7 +885,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._horizontalScrollbar = undefined; } } - this._xterm?.dispose(); + this.xterm?.dispose(); if (this._pressAnyKeyToCloseListener) { this._pressAnyKeyToCloseListener.dispose(); @@ -1097,17 +908,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { await this._processManager.detachFromProcess(); } - forceRedraw(): void { - if (!this._xterm) { - return; - } - this._webglAddon?.clearTextureAtlas(); - this._xterm?.clearTextureAtlas(); - } - focus(force?: boolean): void { this._refreshAltBufferContextKey(); - if (!this._xterm) { + if (!this.xterm) { return; } const selection = window.getSelection(); @@ -1116,7 +919,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } const text = selection.toString(); if (!text || force) { - this._xterm.focus(); + this.xterm.raw.focus(); } } @@ -1127,19 +930,19 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } async paste(): Promise<void> { - if (!this._xterm) { + if (!this.xterm) { return; } this.focus(); - this._xterm.paste(await this._clipboardService.readText()); + this.xterm.raw.paste(await this._clipboardService.readText()); } async pasteSelection(): Promise<void> { - if (!this._xterm) { + if (!this.xterm) { return; } this.focus(); - this._xterm.paste(await this._clipboardService.readText('selection')); + this.xterm.raw.paste(await this._clipboardService.readText('selection')); } async sendText(text: string, addNewLine: boolean): Promise<void> { @@ -1159,48 +962,47 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (this._wrapperElement) { this._wrapperElement.classList.toggle('active', visible); } - if (visible && this._xterm && this._xtermCore) { + if (visible && this.xterm) { // Resize to re-evaluate dimensions, this will ensure when switching to a terminal it is // using the most up to date dimensions (eg. when terminal is created in the background // using cached dimensions of a split terminal). this._resize(); - // Trigger a forced refresh of the viewport to sync the viewport and scroll bar. This is // necessary if the number of rows in the terminal has decreased while it was in the // background since scrollTop changes take no effect but the terminal's position does // change since the number of visible rows decreases. // This can likely be removed after https://github.com/xtermjs/xterm.js/issues/291 is // fixed upstream. - this._xtermCore.viewport?._innerRefresh(); + this.xterm.forceRefresh(); } } scrollDownLine(): void { - this._xterm?.scrollLines(1); + this.xterm?.scrollDownLine(); } scrollDownPage(): void { - this._xterm?.scrollPages(1); + this.xterm?.scrollDownPage(); } scrollToBottom(): void { - this._xterm?.scrollToBottom(); + this.xterm?.scrollToBottom(); } scrollUpLine(): void { - this._xterm?.scrollLines(-1); + this.xterm?.scrollUpLine(); } scrollUpPage(): void { - this._xterm?.scrollPages(-1); + this.xterm?.scrollUpPage(); } scrollToTop(): void { - this._xterm?.scrollToTop(); + this.xterm?.scrollToTop(); } - clear(): void { - this._xterm?.clear(); + clearBuffer(): void { + this.xterm?.clearBuffer(); } private _refreshSelectionContextKey() { @@ -1229,16 +1031,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._onTitleChanged.fire(this); }); } - this._processManager.onDidChangeProperty(e => { - if (e.type === ProcessPropertyType.Cwd) { - this._cwd = e.value; - this._labelComputer?.refreshLabel(); - } else if (e.type === ProcessPropertyType.InitialCwd) { - this._initialCwd = e.value; - this._cwd = this._initialCwd; - this.refreshTabLabels(this.title, TitleEventSource.Api); - } - }); if (this._shellLaunchConfig.name) { this.refreshTabLabels(this._shellLaunchConfig.name, TitleEventSource.Api); } else { @@ -1246,23 +1038,44 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // _xtermReadyPromise is ready constructed since this is called from the ctor setTimeout(() => { this._xtermReadyPromise.then(xterm => { - this._messageTitleDisposable = xterm.onTitleChange(e => this._onTitleChange(e)); + this._messageTitleDisposable = xterm.raw.onTitleChange(e => this._onTitleChange(e)); }); }); this.refreshTabLabels(this._shellLaunchConfig.executable, TitleEventSource.Process); - this._messageTitleDisposable = this._processManager.onProcessTitle(title => this.refreshTabLabels(title ? title : '', TitleEventSource.Process)); } }); this._processManager.onProcessExit(exitCode => this._onProcessExit(exitCode)); + this._processManager.onDidChangeProperty(({ type, value }) => { + switch (type) { + case ProcessPropertyType.Cwd: + this._cwd = value; + this._labelComputer?.refreshLabel(); + break; + case ProcessPropertyType.InitialCwd: + this._initialCwd = value; + this._cwd = this._initialCwd; + this.refreshTabLabels(this.title, TitleEventSource.Api); + break; + case ProcessPropertyType.Title: + this.refreshTabLabels(value ? value : '', TitleEventSource.Process); + break; + case ProcessPropertyType.OverrideDimensions: + this.setOverrideDimensions(value, true); + break; + case ProcessPropertyType.ResolvedShellLaunchConfig: + this._setResolvedShellLaunchConfig(value); + break; + case ProcessPropertyType.HasChildProcesses: + this._onDidChangeHasChildProcesses.fire(value); + break; + } + }); + this._processManager.onProcessData(ev => { this._initialDataEvents?.push(ev.data); this._onData.fire(ev.data); }); - this._processManager.onProcessOverrideDimensions(e => this.setOverrideDimensions(e, true)); - this._processManager.onProcessResolvedShellLaunchConfig(e => this._setResolvedShellLaunchConfig(e)); - this._processManager.onProcessDidChangeHasChildProcesses(e => this._onDidChangeHasChildProcesses.fire(e)); this._processManager.onEnvironmentVariableInfoChanged(e => this._onEnvironmentVariableInfoChanged(e)); - this._processManager.onProcessShellTypeChanged(type => this.setShellType(type)); this._processManager.onPtyDisconnect(() => { this._safeSetOption('disableStdin', true); this.statusList.add({ @@ -1286,7 +1099,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Re-evaluate dimensions if the container has been set since the xterm instance was created if (this._container && this._cols === 0 && this._rows === 0) { this._initDimensions(); - this._xterm?.resize(this._cols || Constants.DefaultCols, this._rows || Constants.DefaultRows); + this.xterm?.raw.resize(this._cols || Constants.DefaultCols, this._rows || Constants.DefaultRows); } const hadIcon = !!this.shellLaunchConfig.icon; @@ -1304,14 +1117,14 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { const messageId = ++this._latestXtermWriteData; if (ev.trackCommit) { ev.writePromise = new Promise<void>(r => { - this._xterm?.write(ev.data, () => { + this.xterm?.raw.write(ev.data, () => { this._latestXtermParseData = messageId; this._processManager.acknowledgeDataEvent(ev.data.length); r(); }); }); } else { - this._xterm?.write(ev.data, () => { + this.xterm?.raw.write(ev.data, () => { this._latestXtermParseData = messageId; this._processManager.acknowledgeDataEvent(ev.data.length); }); @@ -1401,15 +1214,15 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (this._shellLaunchConfig.waitOnExit && this._processManager.processState !== ProcessState.KilledByUser) { this._xtermReadyPromise.then(xterm => { if (exitCodeMessage) { - xterm.writeln(exitCodeMessage); + xterm.raw.writeln(exitCodeMessage); } if (typeof this._shellLaunchConfig.waitOnExit === 'string') { - xterm.write(formatMessageForTerminal(this._shellLaunchConfig.waitOnExit)); + xterm.raw.write(formatMessageForTerminal(this._shellLaunchConfig.waitOnExit)); } // Disable all input if the terminal is exiting and listen for next keypress - xterm.setOption('disableStdin', true); - if (xterm.textarea) { - this._attachPressAnyKeyToCloseListener(xterm); + xterm.raw.setOption('disableStdin', true); + if (xterm.raw.textarea) { + this._attachPressAnyKeyToCloseListener(xterm.raw); } }); } else { @@ -1476,20 +1289,20 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._pressAnyKeyToCloseListener?.dispose(); this._pressAnyKeyToCloseListener = undefined; - if (this._xterm) { + if (this.xterm) { if (!reset) { // Ensure new processes' output starts at start of new line - await new Promise<void>(r => this._xterm!.write('\n\x1b[G', r)); + await new Promise<void>(r => this.xterm!.raw.write('\n\x1b[G', r)); } // Print initialText if specified if (shell.initialText) { - await new Promise<void>(r => this._xterm!.writeln(shell.initialText!, r)); + await new Promise<void>(r => this.xterm!.raw.writeln(shell.initialText!, r)); } // Clean up waitOnExit state if (this._isExiting && this._shellLaunchConfig.waitOnExit) { - this._xterm.setOption('disableStdin', false); + this.xterm.raw.setOption('disableStdin', false); this._isExiting = false; } } @@ -1512,7 +1325,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._processManager.relaunch(this._shellLaunchConfig, this._cols || Constants.DefaultCols, this._rows || Constants.DefaultRows, this._accessibilityService.isScreenReaderOptimized(), reset); - this._xtermTypeAhead?.reset(); + this._xtermTypeAheadAddon?.reset(); } @debounce(1000) @@ -1556,124 +1369,24 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } updateConfig(): void { - const config = this._configHelper.config; - this._safeSetOption('altClickMovesCursor', config.altClickMovesCursor); - this._setCursorBlink(config.cursorBlinking); - this._setCursorStyle(config.cursorStyle); - this._setCursorWidth(config.cursorWidth); - this._setCommandsToSkipShell(config.commandsToSkipShell); - this._safeSetOption('scrollback', config.scrollback); - this._safeSetOption('drawBoldTextInBrightColors', config.drawBoldTextInBrightColors); - this._safeSetOption('minimumContrastRatio', config.minimumContrastRatio); - this._safeSetOption('fastScrollSensitivity', config.fastScrollSensitivity); - this._safeSetOption('scrollSensitivity', config.mouseWheelScrollSensitivity); - this._safeSetOption('macOptionIsMeta', config.macOptionIsMeta); - const editorOptions = this._configurationService.getValue<IEditorOptions>('editor'); - this._safeSetOption('altClickMovesCursor', config.altClickMovesCursor && editorOptions.multiCursorModifier === 'alt'); - this._safeSetOption('macOptionClickForcesSelection', config.macOptionClickForcesSelection); - this._safeSetOption('rightClickSelectsWord', config.rightClickBehavior === 'selectWord'); - this._safeSetOption('wordSeparator', config.wordSeparators); - this._safeSetOption('customGlyphs', config.customGlyphs); - const suggestedRendererType = TerminalInstance._suggestedRendererType; - // @meganrogge @Tyriar remove if the issue related to iPads and webgl is resolved - if ((!isSafari && config.gpuAcceleration === 'auto' && suggestedRendererType === undefined) || config.gpuAcceleration === 'on') { - this._enableWebglRenderer(); - } else { - this._disposeOfWebglRenderer(); - this._safeSetOption('rendererType', this._getBuiltInXtermRenderer(config.gpuAcceleration, suggestedRendererType)); - } + this._setCommandsToSkipShell(this._configHelper.config.commandsToSkipShell); this._refreshEnvironmentVariableInfoWidgetState(this._processManager.environmentVariableInfo); } - private _getBuiltInXtermRenderer(gpuAcceleration: string, suggestedRendererType?: string): RendererType { - let rendererType: RendererType = 'canvas'; - if (gpuAcceleration === 'off' || (gpuAcceleration === 'auto' && suggestedRendererType === 'dom')) { - rendererType = 'dom'; - } - return rendererType; - } - - private async _enableWebglRenderer(): Promise<void> { - if (!this._xterm?.element || this._webglAddon) { - return; - } - const Addon = await this._terminalInstanceService.getXtermWebglConstructor(); - this._webglAddon = new Addon(); - try { - this._xterm.loadAddon(this._webglAddon); - this._webglAddon.onContextLoss(() => { - this._logService.info(`Webgl lost context, disposing of webgl renderer`); - this._disposeOfWebglRenderer(); - this._safeSetOption('rendererType', 'dom'); - }); - } catch (e) { - this._logService.warn(`Webgl could not be loaded. Falling back to the canvas renderer type.`, e); - const neverMeasureRenderTime = this._storageService.getBoolean(TerminalStorageKeys.NeverMeasureRenderTime, StorageScope.GLOBAL, false); - // if it's already set to dom, no need to measure render time - if (!neverMeasureRenderTime && this._configHelper.config.gpuAcceleration !== 'off') { - this._measureRenderTime(); - } - this._safeSetOption('rendererType', 'canvas'); - TerminalInstance._suggestedRendererType = 'canvas'; - this._disposeOfWebglRenderer(); - } - } - - private _disposeOfWebglRenderer(): void { - try { - this._webglAddon?.dispose(); - } catch { - // ignore - } - this._webglAddon = undefined; - } - private async _updateUnicodeVersion(): Promise<void> { - if (!this._xterm) { - throw new Error('Cannot update unicode version before xterm has been initialized'); - } - if (!this._xtermUnicode11 && this._configHelper.config.unicodeVersion === '11') { - const Addon = await this._terminalInstanceService.getXtermUnicode11Constructor(); - this._xtermUnicode11 = new Addon(); - this._xterm.loadAddon(this._xtermUnicode11); - } - if (this._xterm.unicode.activeVersion !== this._configHelper.config.unicodeVersion) { - this._xterm.unicode.activeVersion = this._configHelper.config.unicodeVersion; - this._processManager.setUnicodeVersion(this._configHelper.config.unicodeVersion); - } + this._processManager.setUnicodeVersion(this._configHelper.config.unicodeVersion); } updateAccessibilitySupport(): void { const isEnabled = this._accessibilityService.isScreenReaderOptimized(); if (isEnabled) { this._navigationModeAddon = new NavigationModeAddon(this._terminalA11yTreeFocusContextKey); - this._xterm!.loadAddon(this._navigationModeAddon); + this.xterm!.raw.loadAddon(this._navigationModeAddon); } else { this._navigationModeAddon?.dispose(); this._navigationModeAddon = undefined; } - this._xterm!.setOption('screenReaderMode', isEnabled); - } - - private _setCursorBlink(blink: boolean): void { - if (this._xterm && this._xterm.getOption('cursorBlink') !== blink) { - this._xterm.setOption('cursorBlink', blink); - this._xterm.refresh(0, this._xterm.rows - 1); - } - } - - private _setCursorStyle(style: string): void { - if (this._xterm && this._xterm.getOption('cursorStyle') !== style) { - // 'line' is used instead of bar in VS Code to be consistent with editor.cursorStyle - const xtermOption = style === 'line' ? 'bar' : style; - this._xterm.setOption('cursorStyle', xtermOption); - } - } - - private _setCursorWidth(width: number): void { - if (this._xterm && this._xterm.getOption('cursorWidth') !== width) { - this._xterm.setOption('cursorWidth', width); - } + this.xterm!.raw.setOption('screenReaderMode', isEnabled); } private _setCommandsToSkipShell(commands: string[]): void { @@ -1684,12 +1397,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } private _safeSetOption(key: string, value: any): void { - if (!this._xterm) { + if (!this.xterm) { return; } - if (this._xterm.getOption(key) !== value) { - this._xterm.setOption(key, value); + if (this.xterm.raw.getOption(key) !== value) { + this.xterm.raw.setOption(key, value); } } @@ -1725,11 +1438,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { let cols = this.cols; let rows = this.rows; - if (this._xterm && this._xtermCore) { + if (this.xterm) { // Only apply these settings when the terminal is visible so that // the characters are measured correctly. if (this._isVisible) { - const font = this._configHelper.getFont(this._xtermCore); + const font = this.xterm ? this.xterm.getFont() : this._configHelper.getFont(); const config = this._configHelper.config; this._safeSetOption('letterSpacing', font.letterSpacing); this._safeSetOption('lineHeight', font.lineHeight); @@ -1749,27 +1462,18 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return; } - if (cols !== this._xterm.cols || rows !== this._xterm.rows) { + if (cols !== this.xterm.raw.cols || rows !== this.xterm.raw.rows) { if (this._fixedRows || this._fixedCols) { await this.updateProperty(ProcessPropertyType.FixedDimensions, { cols: this._fixedCols, rows: this._fixedRows }); } this._onDimensionsChanged.fire(); } - this._xterm.resize(cols, rows); + this.xterm.raw.resize(cols, rows); TerminalInstance._lastKnownGridDimensions = { cols, rows }; if (this._isVisible) { - // HACK: Force the renderer to unpause by simulating an IntersectionObserver event. - // This is to fix an issue where dragging the windpow to the top of the screen to - // maximize on Windows/Linux would fire an event saying that the terminal was not - // visible. - if (this._xterm.getOption('rendererType') === 'canvas') { - this._xtermCore._renderService?._onIntersectionChange({ intersectionRatio: 1 }); - // HACK: Force a refresh of the screen to ensure links are refresh corrected. - // This can probably be removed when the above hack is fixed in Chromium. - this._xterm.refresh(0, this._xterm.rows - 1); - } + this.xterm.forceUnpause(); } } @@ -1807,7 +1511,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { const titleChanged = title !== this._title; this._title = title; this._labelComputer?.refreshLabel(); - this._setAriaLabel(this._xterm, this._instanceId, this._title); + this._setAriaLabel(this.xterm?.raw, this._instanceId, this._title); if (this._titleReadyComplete) { this._titleReadyComplete(title); @@ -1919,7 +1623,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } async toggleSizeToContentWidth(): Promise<void> { - if (!this._xterm?.buffer.active) { + if (!this.xterm?.raw.buffer.active) { return; } if (this._hasScrollBar) { @@ -1932,12 +1636,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._horizontalScrollbar?.setScrollDimensions({ scrollWidth: 0 }); } else { let maxCols = 0; - if (!this._xterm.buffer.active.getLine(0)) { + if (!this.xterm.raw.buffer.active.getLine(0)) { return; } - const lineWidth = this._xterm.buffer.active.getLine(0)!.length; - for (let i = this._xterm.buffer.active.length - 1; i >= this._xterm.buffer.active.viewportY; i--) { - const lineInfo = this._getWrappedLineCount(i, this._xterm.buffer.active); + const lineWidth = this.xterm.raw.buffer.active.getLine(0)!.length; + for (let i = this.xterm.raw.buffer.active.length - 1; i >= this.xterm.raw.buffer.active.viewportY; i--) { + const lineInfo = this._getWrappedLineCount(i, this.xterm.raw.buffer.active); maxCols = Math.max(maxCols, ((lineInfo.lineCount * lineWidth) - lineInfo.endSpaces) || 0); i = lineInfo.currentIndex; } @@ -1949,11 +1653,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } private async _addScrollbar(): Promise<void> { - const charWidth = this._configHelper?.getFont(this._xtermCore).charWidth; - if (!this._xterm?.element || !this._wrapperElement || !this._container || !charWidth || !this._fixedCols) { + const charWidth = (this.xterm ? this.xterm.getFont() : this._configHelper.getFont()).charWidth; + if (!this.xterm?.raw.element || !this._wrapperElement || !this._container || !charWidth || !this._fixedCols) { return; } - if (this._fixedCols < this._xterm.buffer.active.getLine(0)!.length) { + if (this._fixedCols < this.xterm.raw.buffer.active.getLine(0)!.length) { // no scrollbar needed return; } @@ -1974,14 +1678,14 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } this._horizontalScrollbar.setScrollDimensions( { - width: this._xterm.element.clientWidth, + width: this.xterm.raw.element.clientWidth, scrollWidth: this._fixedCols * charWidth }); this._horizontalScrollbar!.getDomNode().style.paddingBottom = '16px'; // work around for https://github.com/xtermjs/xterm.js/issues/3482 - for (let i = this._xterm.buffer.active.viewportY; i < this._xterm.buffer.active.length; i++) { - let line = this._xterm.buffer.active.getLine(i); + for (let i = this.xterm.raw.buffer.active.viewportY; i < this.xterm.raw.buffer.active.length; i++) { + let line = this.xterm.raw.buffer.active.getLine(i); (line as any)._line.isWrapped = false; } } @@ -2022,7 +1726,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _onEnvironmentVariableInfoChanged(info: IEnvironmentVariableInfo): void { if (info.requiresAction) { - this._xterm?.textarea?.setAttribute('aria-label', nls.localize('terminalStaleTextBoxAriaLabel', "Terminal {0} environment is stale, run the 'Show Environment Information' command for more information", this._instanceId)); + this.xterm?.raw.textarea?.setAttribute('aria-label', nls.localize('terminalStaleTextBoxAriaLabel', "Terminal {0} environment is stale, run the 'Show Environment Information' command for more information", this._instanceId)); } this._refreshEnvironmentVariableInfoWidgetState(info); } @@ -2073,56 +1777,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } - private _getXtermTheme(theme?: IColorTheme): ITheme { - if (!theme) { - theme = this._themeService.getColorTheme(); - } - - const location = this._viewDescriptorService.getViewLocationById(TERMINAL_VIEW_ID)!; - const foregroundColor = theme.getColor(TERMINAL_FOREGROUND_COLOR); - let backgroundColor: Color | undefined; - if (this.target === TerminalLocation.Editor) { - backgroundColor = theme.getColor(TERMINAL_BACKGROUND_COLOR) || theme.getColor(editorBackground); - } else { - backgroundColor = theme.getColor(TERMINAL_BACKGROUND_COLOR) || (location === ViewContainerLocation.Sidebar ? theme.getColor(SIDE_BAR_BACKGROUND) : theme.getColor(PANEL_BACKGROUND)); - } - const cursorColor = theme.getColor(TERMINAL_CURSOR_FOREGROUND_COLOR) || foregroundColor; - const cursorAccentColor = theme.getColor(TERMINAL_CURSOR_BACKGROUND_COLOR) || backgroundColor; - const selectionColor = theme.getColor(TERMINAL_SELECTION_BACKGROUND_COLOR); - - return { - background: backgroundColor ? backgroundColor.toString() : undefined, - foreground: foregroundColor ? foregroundColor.toString() : undefined, - cursor: cursorColor ? cursorColor.toString() : undefined, - cursorAccent: cursorAccentColor ? cursorAccentColor.toString() : undefined, - selection: selectionColor ? selectionColor.toString() : undefined, - black: theme.getColor(ansiColorIdentifiers[0])!.toString(), - red: theme.getColor(ansiColorIdentifiers[1])!.toString(), - green: theme.getColor(ansiColorIdentifiers[2])!.toString(), - yellow: theme.getColor(ansiColorIdentifiers[3])!.toString(), - blue: theme.getColor(ansiColorIdentifiers[4])!.toString(), - magenta: theme.getColor(ansiColorIdentifiers[5])!.toString(), - cyan: theme.getColor(ansiColorIdentifiers[6])!.toString(), - white: theme.getColor(ansiColorIdentifiers[7])!.toString(), - brightBlack: theme.getColor(ansiColorIdentifiers[8])!.toString(), - brightRed: theme.getColor(ansiColorIdentifiers[9])!.toString(), - brightGreen: theme.getColor(ansiColorIdentifiers[10])!.toString(), - brightYellow: theme.getColor(ansiColorIdentifiers[11])!.toString(), - brightBlue: theme.getColor(ansiColorIdentifiers[12])!.toString(), - brightMagenta: theme.getColor(ansiColorIdentifiers[13])!.toString(), - brightCyan: theme.getColor(ansiColorIdentifiers[14])!.toString(), - brightWhite: theme.getColor(ansiColorIdentifiers[15])!.toString() - }; - } - - private _updateTheme(xterm: XTermTerminal, theme?: IColorTheme): void { - xterm.setOption('theme', this._getXtermTheme(theme)); - } - async toggleEscapeSequenceLogging(): Promise<void> { const xterm = await this._xtermReadyPromise; - const isDebug = xterm.getOption('logLevel') === 'debug'; - xterm.setOption('logLevel', isDebug ? 'info' : 'debug'); + const isDebug = xterm.raw.getOption('logLevel') === 'debug'; + xterm.raw.setOption('logLevel', isDebug ? 'info' : 'debug'); } async getInitialCwd(): Promise<string> { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts index a747adf0ec9..1266362617d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts @@ -7,7 +7,6 @@ import { IRemoteTerminalService, ITerminalInstance, ITerminalInstanceService } f import type { Terminal as XTermTerminal } from 'xterm'; import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; import type { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11'; -import type { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Disposable } from 'vs/base/common/lifecycle'; import { IShellLaunchConfig, ITerminalProfile, TerminalLocation, TerminalShellType, WindowsShellType } from 'vs/platform/terminal/common/terminal'; @@ -26,7 +25,6 @@ import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/termin let Terminal: typeof XTermTerminal; let SearchAddon: typeof XTermSearchAddon; let Unicode11Addon: typeof XTermUnicode11Addon; -let WebglAddon: typeof XTermWebglAddon; export class TerminalInstanceService extends Disposable implements ITerminalInstanceService { declare _serviceBrand: undefined; @@ -121,13 +119,6 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst return Unicode11Addon; } - async getXtermWebglConstructor(): Promise<typeof XTermWebglAddon> { - if (!WebglAddon) { - WebglAddon = (await import('xterm-addon-webgl')).WebglAddon; - } - return WebglAddon; - } - async preparePathForTerminalAsync(originalPath: string, executable: string | undefined, title: string, shellType: TerminalShellType, isRemote: boolean): Promise<string> { return new Promise<string>(c => { if (!executable) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts index 13adb6c0130..c155aa5c4f1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts @@ -5,7 +5,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalDimensionsOverride, ITerminalLaunchError, IProcessProperty, ProcessPropertyType, TerminalShellType, ProcessCapability } from 'vs/platform/terminal/common/terminal'; +import { IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalLaunchError, IProcessProperty, ProcessPropertyType, ProcessCapability } from 'vs/platform/terminal/common/terminal'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ITerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -16,16 +16,8 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal get capabilities(): ProcessCapability[] { return this._capabilities; } private readonly _onProcessData = this._register(new Emitter<string>()); readonly onProcessData: Event<string> = this._onProcessData.event; - private readonly _onProcessExit = this._register(new Emitter<number | undefined>()); - readonly onProcessExit: Event<number | undefined> = this._onProcessExit.event; private readonly _onProcessReady = this._register(new Emitter<IProcessReadyEvent>()); get onProcessReady(): Event<IProcessReadyEvent> { return this._onProcessReady.event; } - private readonly _onProcessTitleChanged = this._register(new Emitter<string>()); - readonly onProcessTitleChanged: Event<string> = this._onProcessTitleChanged.event; - private readonly _onProcessOverrideDimensions = this._register(new Emitter<ITerminalDimensionsOverride | undefined>()); - get onProcessOverrideDimensions(): Event<ITerminalDimensionsOverride | undefined> { return this._onProcessOverrideDimensions.event; } - private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter<IShellLaunchConfig>()); - get onProcessResolvedShellLaunchConfig(): Event<IShellLaunchConfig> { return this._onProcessResolvedShellLaunchConfig.event; } private readonly _onStart = this._register(new Emitter<void>()); readonly onStart: Event<void> = this._onStart.event; @@ -45,10 +37,10 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal readonly onRequestCwd: Event<void> = this._onRequestCwd.event; private readonly _onRequestLatency = this._register(new Emitter<void>()); readonly onRequestLatency: Event<void> = this._onRequestLatency.event; - private readonly _onProcessShellTypeChanged = this._register(new Emitter<TerminalShellType>()); - readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; private readonly _onDidChangeProperty = this._register(new Emitter<IProcessProperty<any>>()); readonly onDidChangeProperty = this._onDidChangeProperty.event; + private readonly _onProcessExit = this._register(new Emitter<number | undefined>()); + readonly onProcessExit: Event<number | undefined> = this._onProcessExit.event; private _pendingInitialCwdRequests: ((value: string | PromiseLike<string>) => void)[] = []; @@ -63,31 +55,50 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal ) { super(); } - onDidChangeHasChildProcesses?: Event<boolean> | undefined; emitData(data: string): void { this._onProcessData.fire(data); } emitTitle(title: string): void { - this._onProcessTitleChanged.fire(title); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.Title, value: title }); } emitReady(pid: number, cwd: string): void { this._onProcessReady.fire({ pid, cwd, capabilities: this.capabilities }); } + emitProcessProperty({ type, value }: IProcessProperty<any>): void { + switch (type) { + case ProcessPropertyType.Cwd: + this.emitCwd(value); + break; + case ProcessPropertyType.InitialCwd: + this.emitInitialCwd(value); + break; + case ProcessPropertyType.Title: + this.emitTitle(value); + break; + case ProcessPropertyType.OverrideDimensions: + this.emitOverrideDimensions(value); + break; + case ProcessPropertyType.ResolvedShellLaunchConfig: + this.emitResolvedShellLaunchConfig(value); + break; + } + } + emitExit(exitCode: number | undefined): void { this._onProcessExit.fire(exitCode); this.dispose(); } emitOverrideDimensions(dimensions: ITerminalDimensions | undefined): void { - this._onProcessOverrideDimensions.fire(dimensions); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.OverrideDimensions, value: dimensions }); } emitResolvedShellLaunchConfig(shellLaunchConfig: IShellLaunchConfig): void { - this._onProcessResolvedShellLaunchConfig.fire(shellLaunchConfig); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.ResolvedShellLaunchConfig, value: shellLaunchConfig }); } emitInitialCwd(initialCwd: string): void { @@ -167,8 +178,7 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal } async updateProperty<T extends ProcessPropertyType>(type: ProcessPropertyType, value: any): Promise<void> { - if (type === ProcessPropertyType.FixedDimensions) { - + if (type === ProcessPropertyType.FixedDimensions && type === ProcessPropertyType.FixedDimensions && typeof value !== 'string' && value && ('cols' in value || 'rows' in value)) { } } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 3a240a3ed63..f2d20df71bf 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -22,7 +22,7 @@ import { withNullAsUndefined } from 'vs/base/common/types'; import { EnvironmentVariableInfoChangesActive, EnvironmentVariableInfoStale } from 'vs/workbench/contrib/terminal/browser/environmentVariableInfo'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IEnvironmentVariableInfo, IEnvironmentVariableService, IMergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { IProcessDataEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalEnvironment, ITerminalLaunchError, FlowControlConstants, TerminalShellType, ITerminalDimensions, TerminalSettingId, IProcessReadyEvent, IProcessProperty, ProcessPropertyType, IProcessPropertyMap } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalEnvironment, ITerminalLaunchError, FlowControlConstants, ITerminalDimensions, TerminalSettingId, IProcessReadyEvent, IProcessProperty, ProcessPropertyType, IProcessPropertyMap } from 'vs/platform/terminal/common/terminal'; import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder'; import { localize } from 'vs/nls'; import { formatMessageForTerminal } from 'vs/workbench/contrib/terminal/common/terminalStrings'; @@ -94,22 +94,12 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce readonly onBeforeProcessData = this._onBeforeProcessData.event; private readonly _onProcessData = this._register(new Emitter<IProcessDataEvent>()); readonly onProcessData = this._onProcessData.event; - private readonly _onProcessTitle = this._register(new Emitter<string>()); - readonly onProcessTitle = this._onProcessTitle.event; private readonly _onDidChangeProperty = this._register(new Emitter<IProcessProperty<any>>()); readonly onDidChangeProperty = this._onDidChangeProperty.event; - private readonly _onProcessShellTypeChanged = this._register(new Emitter<TerminalShellType>()); - readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; - private readonly _onProcessExit = this._register(new Emitter<number | undefined>()); - readonly onProcessExit = this._onProcessExit.event; - private readonly _onProcessOverrideDimensions = this._register(new Emitter<ITerminalDimensionsOverride | undefined>()); - readonly onProcessOverrideDimensions = this._onProcessOverrideDimensions.event; - private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter<IShellLaunchConfig>()); - readonly onProcessResolvedShellLaunchConfig = this._onProcessResolvedShellLaunchConfig.event; - private readonly _onProcessDidChangeHasChildProcesses = this._register(new Emitter<boolean>()); - readonly onProcessDidChangeHasChildProcesses = this._onProcessDidChangeHasChildProcesses.event; private readonly _onEnvironmentVariableInfoChange = this._register(new Emitter<IEnvironmentVariableInfo>()); readonly onEnvironmentVariableInfoChanged = this._onEnvironmentVariableInfoChange.event; + private readonly _onProcessExit = this._register(new Emitter<number | undefined>()); + readonly onProcessExit = this._onProcessExit.event; get persistentProcessId(): number | undefined { return this._process?.id; } get shouldPersist(): boolean { return this._process ? this._process.shouldPersist : false; } @@ -324,23 +314,16 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce this._preLaunchInputQueue.length = 0; } }), - newProcess.onProcessTitleChanged(title => this._onProcessTitle.fire(title)), - newProcess.onProcessShellTypeChanged(type => this._onProcessShellTypeChanged.fire(type)), newProcess.onProcessExit(exitCode => this._onExit(exitCode)), - newProcess.onDidChangeProperty(property => this._onDidChangeProperty.fire(property)) + newProcess.onDidChangeProperty(({ type, value }) => { + switch (type) { + case ProcessPropertyType.HasChildProcesses: + this._hasChildProcesses = value; + break; + } + this._onDidChangeProperty.fire({ type, value }); + }) ]; - if (newProcess.onProcessOverrideDimensions) { - this._processListeners.push(newProcess.onProcessOverrideDimensions(e => this._onProcessOverrideDimensions.fire(e))); - } - if (newProcess.onProcessResolvedShellLaunchConfig) { - this._processListeners.push(newProcess.onProcessResolvedShellLaunchConfig(e => this._onProcessResolvedShellLaunchConfig.fire(e))); - } - if (newProcess.onDidChangeHasChildProcesses) { - this._processListeners.push(newProcess.onDidChangeHasChildProcesses(e => { - this._hasChildProcesses = e; - this._onProcessDidChangeHasChildProcesses.fire(e); - })); - } setTimeout(() => { if (this.processState === ProcessState.Launching) { @@ -580,7 +563,6 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce private _onExit(exitCode: number | undefined): void { this._process = null; - // If the process is marked as launching then mark the process as killed // during launch. This typically means that there is a problem with the // shell and args. diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts index e3cda80333d..6ba70f5bb24 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts @@ -9,19 +9,25 @@ import { withNullAsUndefined } from 'vs/base/common/types'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IRemoteTerminalService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IRemoteTerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IProcessEnvironment, OperatingSystem, OS } from 'vs/base/common/platform'; -import { IShellLaunchConfig, ITerminalProfile, TerminalIcon, TerminalSettingId, TerminalSettingPrefix } from 'vs/platform/terminal/common/terminal'; -import { IShellLaunchConfigResolveOptions, ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IShellLaunchConfig, ITerminalProfile, ITerminalProfileObject, TerminalIcon, TerminalSettingId, TerminalSettingPrefix } from 'vs/platform/terminal/common/terminal'; +import { IShellLaunchConfigResolveOptions, ITerminalProfileResolverService, ITerminalProfileService } from 'vs/workbench/contrib/terminal/common/terminal'; import * as path from 'vs/base/common/path'; import { Codicon, iconRegistry } from 'vs/base/common/codicons'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { debounce } from 'vs/base/common/decorators'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { URI, UriComponents } from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { equals } from 'vs/base/common/arrays'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import Severity from 'vs/base/common/severity'; +import { INotificationService, IPromptChoice, NeverShowAgainScope } from 'vs/platform/notification/common/notification'; +import { localize } from 'vs/nls'; +import { deepClone } from 'vs/base/common/objects'; +import { terminalProfileArgsMatch, isUriComponents } from 'vs/platform/terminal/common/terminalProfiles'; export interface IProfileContextProvider { getDefaultSystemShell: (remoteAuthority: string | undefined, os: OperatingSystem) => Promise<string>; @@ -30,6 +36,16 @@ export interface IProfileContextProvider { const generatedProfileName = 'Generated'; +/* +* Resolves terminal shell launch config and terminal +* profiles for the given operating system, +* environment, and user configuration +*/ + +const SHOULD_PROMPT_FOR_PROFILE_MIGRATION_KEY = 'terminals.integrated.profile-migration'; + +let migrationMessageShown = false; + export abstract class BaseTerminalProfileResolverService implements ITerminalProfileResolverService { declare _serviceBrand: undefined; @@ -44,9 +60,11 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro private readonly _configurationResolverService: IConfigurationResolverService, private readonly _historyService: IHistoryService, private readonly _logService: ILogService, - private readonly _terminalService: ITerminalService, + private readonly _terminalProfileService: ITerminalProfileService, private readonly _workspaceContextService: IWorkspaceContextService, - private readonly _remoteAgentService: IRemoteAgentService + private readonly _remoteAgentService: IRemoteAgentService, + private readonly _storageService: IStorageService, + private readonly _notificationService: INotificationService ) { if (this._remoteAgentService.getConnection()) { this._remoteAgentService.getEnvironment().then(env => this._primaryBackendOs = env?.os || OS); @@ -60,7 +78,8 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro this._refreshDefaultProfileName(); } }); - this._terminalService.onDidChangeAvailableProfiles(() => this._refreshDefaultProfileName()); + this._terminalProfileService.onDidChangeAvailableProfiles(() => this._refreshDefaultProfileName()); + this.showProfileMigrationNotification(); } @debounce(200) @@ -132,7 +151,6 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro } } - async getDefaultShell(options: IShellLaunchConfigResolveOptions): Promise<string> { return (await this.getDefaultProfile(options)).path; } @@ -159,26 +177,18 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro if (ThemeIcon.isThemeIcon(icon)) { return icon; } - if (URI.isUri(icon) || this._isUriComponents(icon)) { + if (URI.isUri(icon) || isUriComponents(icon)) { return URI.revive(icon); } if (typeof icon === 'object' && icon && 'light' in icon && 'dark' in icon) { const castedIcon = (icon as { light: unknown, dark: unknown }); - if ((URI.isUri(castedIcon.light) || this._isUriComponents(castedIcon.light)) && (URI.isUri(castedIcon.dark) || this._isUriComponents(castedIcon.dark))) { + if ((URI.isUri(castedIcon.light) || isUriComponents(castedIcon.light)) && (URI.isUri(castedIcon.dark) || isUriComponents(castedIcon.dark))) { return { light: URI.revive(castedIcon.light), dark: URI.revive(castedIcon.dark) }; } } return undefined; } - private _isUriComponents(thing: unknown): thing is UriComponents { - if (!thing) { - return false; - } - return typeof (<any>thing).path === 'string' && - typeof (<any>thing).scheme === 'string'; - } - private async _getUnresolvedDefaultProfile(options: IShellLaunchConfigResolveOptions): Promise<ITerminalProfile> { // If automation shell is allowed, prefer that if (options.allowAutomationShell) { @@ -192,26 +202,35 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro // allow users to migrate, see https://github.com/microsoft/vscode/issues/123171 const shellSettingProfile = await this._getUnresolvedShellSettingDefaultProfile(options); if (shellSettingProfile) { - return shellSettingProfile; + return this._setIconForAutomation(options, shellSettingProfile); } // Return the real default profile if it exists and is valid, wait for profiles to be ready // if the window just opened - await this._terminalService.profilesReady; + await this._terminalProfileService.profilesReady; const defaultProfile = this._getUnresolvedRealDefaultProfile(options.os); if (defaultProfile) { - return defaultProfile; + return this._setIconForAutomation(options, defaultProfile); } // If there is no real default profile, create a fallback default profile based on the shell // and shellArgs settings in addition to the current environment. - return this._getUnresolvedFallbackDefaultProfile(options); + return this._setIconForAutomation(options, await this._getUnresolvedFallbackDefaultProfile(options)); + } + + private _setIconForAutomation(options: IShellLaunchConfigResolveOptions, profile: ITerminalProfile): ITerminalProfile { + if (options.allowAutomationShell) { + const profileClone = deepClone(profile); + profileClone.icon = Codicon.tools; + return profileClone; + } + return profile; } private _getUnresolvedRealDefaultProfile(os: OperatingSystem): ITerminalProfile | undefined { const defaultProfileName = this._configurationService.getValue(`${TerminalSettingPrefix.DefaultProfile}${this._getOsKey(os)}`); if (defaultProfileName && typeof defaultProfileName === 'string') { - return this._terminalService.availableProfiles.find(e => e.profileName === defaultProfileName); + return this._terminalProfileService.availableProfiles.find(e => e.profileName === defaultProfileName); } return undefined; } @@ -260,8 +279,12 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro const executable = await this._context.getDefaultSystemShell(options.remoteAuthority, options.os); // Try select an existing profile to fallback to, based on the default system shell - const existingProfile = this._terminalService.availableProfiles.find(e => path.parse(e.path).name === path.parse(executable).name); + let existingProfile = this._terminalProfileService.availableProfiles.find(e => path.parse(e.path).name === path.parse(executable).name); if (existingProfile) { + if (options.allowAutomationShell) { + existingProfile = deepClone(existingProfile); + existingProfile.icon = Codicon.tools; + } return existingProfile; } @@ -288,14 +311,23 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro private _getUnresolvedAutomationShellProfile(options: IShellLaunchConfigResolveOptions): ITerminalProfile | undefined { const automationShell = this._configurationService.getValue(`terminal.integrated.automationShell.${this._getOsKey(options.os)}`); - if (!automationShell || typeof automationShell !== 'string') { - return undefined; + if (automationShell && typeof automationShell === 'string') { + return { + path: automationShell, + profileName: generatedProfileName, + isDefault: false, + icon: Codicon.tools + }; } - return { - path: automationShell, - profileName: generatedProfileName, - isDefault: false - }; + + // Use automationProfile second + const automationProfile = this._configurationService.getValue(`terminal.integrated.automationProfile.${this._getOsKey(options.os)}`); + if (this._isValidAutomationProfile(automationProfile, options.os)) { + automationProfile.icon = automationProfile.icon ?? Codicon.tools; + return automationProfile; + } + + return undefined; } private async _resolveProfile(profile: ITerminalProfile, options: IShellLaunchConfigResolveOptions): Promise<ITerminalProfile> { @@ -394,7 +426,7 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro } async createProfileFromShellAndShellArgs(shell?: unknown, shellArgs?: unknown): Promise<ITerminalProfile | string> { - const detectedProfile = this._terminalService.availableProfiles?.find(p => { + const detectedProfile = this._terminalProfileService.availableProfiles?.find(p => { if (p.path !== shell) { return false; } @@ -416,30 +448,63 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro args, isDefault: true }; - if (detectedProfile && detectedProfile.profileName === createdProfile.profileName && detectedProfile.path === createdProfile.path && this._argsMatch(detectedProfile.args, createdProfile.args)) { + if (detectedProfile && detectedProfile.profileName === createdProfile.profileName && detectedProfile.path === createdProfile.path && terminalProfileArgsMatch(detectedProfile.args, createdProfile.args)) { return detectedProfile.profileName; } return createdProfile; } - private _argsMatch(args1: string | string[] | undefined, args2: string | string[] | undefined): boolean { - if (!args1 && !args2) { - return true; - } else if (typeof args1 === 'string' && typeof args2 === 'string') { - return args1 === args2; - } else if (Array.isArray(args1) && Array.isArray(args2)) { - if (args1.length !== args2.length) { - return false; - } - for (let i = 0; i < args1.length; i++) { - if (args1[i] !== args2[i]) { - return false; - } - } + private _isValidAutomationProfile(profile: unknown, os: OperatingSystem): profile is ITerminalProfile { + if (!profile === undefined || typeof profile !== 'object' || profile === null) { + return false; + } + if ('path' in profile && typeof (profile as { path: unknown }).path === 'string') { return true; } return false; } + + async showProfileMigrationNotification(): Promise<void> { + const shouldMigrateToProfile = (!!this._configurationService.getValue(TerminalSettingPrefix.Shell + this._primaryBackendOs) || + !!this._configurationService.inspect(TerminalSettingPrefix.ShellArgs + this._primaryBackendOs).userValue) && + !!this._configurationService.getValue(TerminalSettingPrefix.DefaultProfile + this._primaryBackendOs); + if (shouldMigrateToProfile && this._storageService.getBoolean(SHOULD_PROMPT_FOR_PROFILE_MIGRATION_KEY, StorageScope.WORKSPACE, true) && !migrationMessageShown) { + this._notificationService.prompt( + Severity.Info, + localize('terminalProfileMigration', "The terminal is using deprecated shell/shellArgs settings, do you want to migrate it to a profile?"), + [ + { + label: localize('migrateToProfile', "Migrate"), + run: async () => { + const shell = this._configurationService.getValue(TerminalSettingPrefix.Shell + this._primaryBackendOs); + const shellArgs = this._configurationService.getValue(TerminalSettingPrefix.ShellArgs + this._primaryBackendOs); + const profile = await this.createProfileFromShellAndShellArgs(shell, shellArgs); + if (typeof profile === 'string') { + await this._configurationService.updateValue(TerminalSettingPrefix.DefaultProfile + this._primaryBackendOs, profile); + this._logService.trace(`migrated from shell/shellArgs, using existing profile ${profile}`); + } else { + const profiles = { ...this._configurationService.inspect<Readonly<{ [key: string]: ITerminalProfileObject }>>(TerminalSettingPrefix.Profiles + this._primaryBackendOs).userValue } || {}; + const profileConfig: ITerminalProfileObject = { path: profile.path }; + if (profile.args) { + profileConfig.args = profile.args; + } + profiles[profile.profileName] = profileConfig; + await this._configurationService.updateValue(TerminalSettingPrefix.Profiles + this._primaryBackendOs, profiles); + await this._configurationService.updateValue(TerminalSettingPrefix.DefaultProfile + this._primaryBackendOs, profile.profileName); + this._logService.trace(`migrated from shell/shellArgs, ${shell} ${shellArgs} to profile ${JSON.stringify(profile)}`); + } + await this._configurationService.updateValue(TerminalSettingPrefix.Shell + this._primaryBackendOs, undefined); + await this._configurationService.updateValue(TerminalSettingPrefix.ShellArgs + this._primaryBackendOs, undefined); + } + } as IPromptChoice, + ], + { + neverShowAgain: { id: SHOULD_PROMPT_FOR_PROFILE_MIGRATION_KEY, scope: NeverShowAgainScope.WORKSPACE } + } + ); + migrationMessageShown = true; + } + } } export class BrowserTerminalProfileResolverService extends BaseTerminalProfileResolverService { @@ -450,9 +515,11 @@ export class BrowserTerminalProfileResolverService extends BaseTerminalProfileRe @IHistoryService historyService: IHistoryService, @ILogService logService: ILogService, @IRemoteTerminalService remoteTerminalService: IRemoteTerminalService, - @ITerminalService terminalService: ITerminalService, + @ITerminalProfileService terminalProfileService: ITerminalProfileService, @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, - @IRemoteAgentService remoteAgentService: IRemoteAgentService + @IRemoteAgentService remoteAgentService: IRemoteAgentService, + @IStorageService storageService: IStorageService, + @INotificationService notificationService: INotificationService ) { super( { @@ -474,9 +541,11 @@ export class BrowserTerminalProfileResolverService extends BaseTerminalProfileRe configurationResolverService, historyService, logService, - terminalService, + terminalProfileService, workspaceContextService, - remoteAgentService + remoteAgentService, + storageService, + notificationService ); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts new file mode 100644 index 00000000000..324bef51c71 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts @@ -0,0 +1,225 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { equals } from 'vs/base/common/arrays'; +import { AutoOpenBarrier } from 'vs/base/common/async'; +import { throttle } from 'vs/base/common/decorators'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { isMacintosh, isWeb, isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; +import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { optional } from 'vs/platform/instantiation/common/instantiation'; +import { ITerminalProfile, IExtensionTerminalProfile, TerminalSettingPrefix, TerminalSettingId, ICreateContributedTerminalProfileOptions, ITerminalProfileObject, IShellLaunchConfig } from 'vs/platform/terminal/common/terminal'; +import { registerTerminalDefaultProfileConfiguration } from 'vs/platform/terminal/common/terminalPlatformConfiguration'; +import { terminalIconsEqual, terminalProfileArgsMatch } from 'vs/platform/terminal/common/terminalProfiles'; +import { IRemoteTerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { refreshTerminalActions } from 'vs/workbench/contrib/terminal/browser/terminalActions'; +import { ILocalTerminalService, IOffProcessTerminalService, ITerminalProfileProvider, ITerminalProfileService } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { ITerminalContributionService } from 'vs/workbench/contrib/terminal/common/terminalExtensionPoints'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; + +/* +* Links TerminalService with TerminalProfileResolverService +* and keeps the available terminal profiles updated +*/ +export class TerminalProfileService implements ITerminalProfileService { + private _ifNoProfilesTryAgain: boolean = true; + private _webExtensionContributedProfileContextKey: IContextKey<boolean>; + private _profilesReadyBarrier: AutoOpenBarrier; + private _availableProfiles: ITerminalProfile[] | undefined; + private _contributedProfiles: IExtensionTerminalProfile[] = []; + private _defaultProfileName?: string; + private readonly _profileProviders: Map</*ext id*/string, Map</*provider id*/string, ITerminalProfileProvider>> = new Map(); + private readonly _primaryOffProcessTerminalService?: IOffProcessTerminalService; + + private readonly _onDidChangeAvailableProfiles = new Emitter<ITerminalProfile[]>(); + get onDidChangeAvailableProfiles(): Event<ITerminalProfile[]> { return this._onDidChangeAvailableProfiles.event; } + + get profilesReady(): Promise<void> { return this._profilesReadyBarrier.wait().then(() => { }); } + get availableProfiles(): ITerminalProfile[] { + this.refreshAvailableProfiles(); + return this._availableProfiles || []; + } + get contributedProfiles(): IExtensionTerminalProfile[] { + return this._contributedProfiles || []; + } + constructor( + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @ITerminalContributionService private readonly _terminalContributionService: ITerminalContributionService, + @IExtensionService private readonly _extensionService: IExtensionService, + @IRemoteAgentService private _remoteAgentService: IRemoteAgentService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, + @IRemoteTerminalService private readonly _remoteTerminalService: IRemoteTerminalService, + @optional(ILocalTerminalService) private readonly _localTerminalService: ILocalTerminalService + ) { + // in web, we don't want to show the dropdown unless there's a web extension + // that contributes a profile + this._extensionService.onDidChangeExtensions(() => this.refreshAvailableProfiles()); + + this._configurationService.onDidChangeConfiguration(async e => { + const platformKey = await this._getPlatformKey(); + if (e.affectsConfiguration(TerminalSettingPrefix.DefaultProfile + platformKey) || + e.affectsConfiguration(TerminalSettingPrefix.Profiles + platformKey) || + e.affectsConfiguration(TerminalSettingId.UseWslProfiles)) { + this.refreshAvailableProfiles(); + } + }); + this._webExtensionContributedProfileContextKey = TerminalContextKeys.webExtensionContributedProfile.bindTo(this._contextKeyService); + + this._primaryOffProcessTerminalService = !!this._environmentService.remoteAuthority ? this._remoteTerminalService : (this._localTerminalService || this._remoteTerminalService); + // Wait up to 5 seconds for profiles to be ready so it's assured that we know the actual + // default terminal before launching the first terminal. This isn't expected to ever take + // this long. + this._profilesReadyBarrier = new AutoOpenBarrier(5000); + this.refreshAvailableProfiles(); + } + + _serviceBrand: undefined; + + getDefaultProfileName(): string { + if (!this._defaultProfileName) { + throw new Error('no default profile'); + } + return this._defaultProfileName; + } + + @throttle(2000) + refreshAvailableProfiles(): void { + this._refreshAvailableProfilesNow(); + } + + protected async _refreshAvailableProfilesNow(): Promise<void> { + const profiles = await this._detectProfiles(); + if (profiles.length === 0 && this._ifNoProfilesTryAgain) { + // available profiles get updated when a terminal is created + // or relevant config changes. + // if there are no profiles, we want to refresh them again + // since terminal creation can't happen in this case and users + // might not think to try changing the config + this._ifNoProfilesTryAgain = false; + this._refreshAvailableProfilesNow(); + } + const profilesChanged = !(equals(profiles, this._availableProfiles, profilesEqual)); + const contributedProfilesChanged = await this._updateContributedProfiles(); + if (profilesChanged || contributedProfilesChanged) { + this._availableProfiles = profiles; + this._onDidChangeAvailableProfiles.fire(this._availableProfiles); + this._profilesReadyBarrier.open(); + this._updateWebContextKey(); + await this._refreshPlatformConfig(profiles); + } + } + + private async _updateContributedProfiles(): Promise<boolean> { + const platformKey = await this._getPlatformKey(); + const excludedContributedProfiles: string[] = []; + const configProfiles: { [key: string]: any } = this._configurationService.getValue(TerminalSettingPrefix.Profiles + platformKey); + for (const [profileName, value] of Object.entries(configProfiles)) { + if (value === null) { + excludedContributedProfiles.push(profileName); + } + } + const filteredContributedProfiles = Array.from(this._terminalContributionService.terminalProfiles.filter(p => !excludedContributedProfiles.includes(p.title))); + const contributedProfilesChanged = !equals(filteredContributedProfiles, this._contributedProfiles, contributedProfilesEqual); + this._contributedProfiles = filteredContributedProfiles; + return contributedProfilesChanged; + } + + getContributedProfileProvider(extensionIdentifier: string, id: string): ITerminalProfileProvider | undefined { + const extMap = this._profileProviders.get(extensionIdentifier); + return extMap?.get(id); + } + + private async _detectProfiles(includeDetectedProfiles?: boolean): Promise<ITerminalProfile[]> { + if (!this._primaryOffProcessTerminalService) { + return this._availableProfiles || []; + } + const platform = await this._getPlatformKey(); + this._defaultProfileName = this._configurationService.getValue(`${TerminalSettingPrefix.DefaultProfile}${platform}`); + return this._primaryOffProcessTerminalService?.getProfiles(this._configurationService.getValue(`${TerminalSettingPrefix.Profiles}${platform}`), this._defaultProfileName, includeDetectedProfiles); + } + + private _updateWebContextKey(): void { + this._webExtensionContributedProfileContextKey.set(isWeb && this._contributedProfiles.length > 0); + } + + private async _refreshPlatformConfig(profiles: ITerminalProfile[]) { + const env = await this._remoteAgentService.getEnvironment(); + registerTerminalDefaultProfileConfiguration({ os: env?.os || OS, profiles }, this._contributedProfiles); + refreshTerminalActions(profiles); + } + + private async _getPlatformKey(): Promise<string> { + const env = await this._remoteAgentService.getEnvironment(); + if (env) { + return env.os === OperatingSystem.Windows ? 'windows' : (env.os === OperatingSystem.Macintosh ? 'osx' : 'linux'); + } + return isWindows ? 'windows' : (isMacintosh ? 'osx' : 'linux'); + } + + registerTerminalProfileProvider(extensionIdentifier: string, id: string, profileProvider: ITerminalProfileProvider): IDisposable { + let extMap = this._profileProviders.get(extensionIdentifier); + if (!extMap) { + extMap = new Map(); + this._profileProviders.set(extensionIdentifier, extMap); + } + extMap.set(id, profileProvider); + return toDisposable(() => this._profileProviders.delete(id)); + } + + async registerContributedProfile(extensionIdentifier: string, id: string, title: string, options: ICreateContributedTerminalProfileOptions): Promise<void> { + const platformKey = await this._getPlatformKey(); + const profilesConfig = await this._configurationService.getValue(`${TerminalSettingPrefix.Profiles}${platformKey}`); + if (typeof profilesConfig === 'object') { + const newProfile: IExtensionTerminalProfile = { + extensionIdentifier: extensionIdentifier, + icon: options.icon, + id, + title: title, + color: options.color + }; + + (profilesConfig as { [key: string]: ITerminalProfileObject })[title] = newProfile; + } + await this._configurationService.updateValue(`${TerminalSettingPrefix.Profiles}${platformKey}`, profilesConfig, ConfigurationTarget.USER); + return; + } + + async getContributedDefaultProfile(shellLaunchConfig: IShellLaunchConfig): Promise<IExtensionTerminalProfile | undefined> { + // prevents recursion with the MainThreadTerminalService call to create terminal + // and defers to the provided launch config when an executable is provided + if (shellLaunchConfig && !shellLaunchConfig.extHostTerminalId && !('executable' in shellLaunchConfig)) { + const key = await this._getPlatformKey(); + const defaultProfileName = this._configurationService.getValue(`${TerminalSettingPrefix.DefaultProfile}${key}`); + const contributedDefaultProfile = this.contributedProfiles.find(p => p.title === defaultProfileName); + return contributedDefaultProfile; + } + return undefined; + } +} + +function profilesEqual(one: ITerminalProfile, other: ITerminalProfile) { + return one.profileName === other.profileName && + terminalProfileArgsMatch(one.args, other.args) && + one.color === other.color && + terminalIconsEqual(one.icon, other.icon) && + one.isAutoDetected === other.isAutoDetected && + one.isDefault === other.isDefault && + one.overrideName === other.overrideName && + one.path === other.path; +} + +function contributedProfilesEqual(one: IExtensionTerminalProfile, other: IExtensionTerminalProfile) { + return one.extensionIdentifier === other.extensionIdentifier && + one.color === other.color && + one.icon === other.icon && + one.id === other.id && + one.title === other.title; +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index f001095e567..1af150af022 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -4,14 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { AutoOpenBarrier, timeout } from 'vs/base/common/async'; +import { timeout } from 'vs/base/common/async'; import { Codicon, iconRegistry } from 'vs/base/common/codicons'; -import { debounce, throttle } from 'vs/base/common/decorators'; +import { debounce } from 'vs/base/common/decorators'; import { Emitter, Event } from 'vs/base/common/event'; import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; -import { equals } from 'vs/base/common/objects'; -import { isMacintosh, isWeb, isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; +import { isMacintosh, isWeb, isWindows, OperatingSystem } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import * as nls from 'vs/nls'; @@ -20,36 +19,33 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; -import { INotificationService } from 'vs/platform/notification/common/notification'; import { IKeyMods, IPickOptions, IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ICreateContributedTerminalProfileOptions, IExtensionTerminalProfile, IShellLaunchConfig, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalLocation, TerminalLocationString, TerminalSettingId, TerminalSettingPrefix } from 'vs/platform/terminal/common/terminal'; -import { registerTerminalDefaultProfileConfiguration } from 'vs/platform/terminal/common/terminalPlatformConfiguration'; +import { ICreateContributedTerminalProfileOptions, IExtensionTerminalProfile, IShellLaunchConfig, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalLocation, TerminalLocationString, TerminalSettingPrefix } from 'vs/platform/terminal/common/terminal'; import { iconForeground } from 'vs/platform/theme/common/colorRegistry'; import { IconDefinition } from 'vs/platform/theme/common/iconRegistry'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { IThemeService, Themable, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { VirtualWorkspaceContext } from 'vs/workbench/browser/contextkeys'; import { IEditableData, IViewsService } from 'vs/workbench/common/views'; -import { ICreateTerminalOptions, IRemoteTerminalService, IRequestAddInstanceToGroupEvent, ITerminalEditorService, ITerminalExternalLinkProvider, ITerminalFindHost, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalLocationOptions, ITerminalProfileProvider, ITerminalService, ITerminalServiceNativeDelegate, TerminalConnectionState, TerminalEditorLocation } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { refreshTerminalActions } from 'vs/workbench/contrib/terminal/browser/terminalActions'; +import { ICreateTerminalOptions, IRemoteTerminalService, IRequestAddInstanceToGroupEvent, ITerminalEditorService, ITerminalExternalLinkProvider, ITerminalFindHost, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalLocationOptions, ITerminalService, ITerminalServiceNativeDelegate, TerminalConnectionState, TerminalEditorLocation } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalEditor } from 'vs/workbench/contrib/terminal/browser/terminalEditor'; import { getColorClass, getColorStyleContent, getColorStyleElement, getUriClasses } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; import { configureTerminalProfileIcon } from 'vs/workbench/contrib/terminal/browser/terminalIcons'; import { getInstanceFromResource, getTerminalUri, parseTerminalUri } from 'vs/workbench/contrib/terminal/browser/terminalUri'; import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; -import { ILocalTerminalService, IOffProcessTerminalService, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalProcessExtHostProxy, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ILocalTerminalService, IOffProcessTerminalService, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalProcessExtHostProxy, ITerminalProfileService, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { ITerminalContributionService } from 'vs/workbench/contrib/terminal/common/terminalExtensionPoints'; import { formatMessageForTerminal, terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ILifecycleService, ShutdownReason, WillShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export class TerminalService implements ITerminalService { declare _serviceBrand: undefined; @@ -57,22 +53,16 @@ export class TerminalService implements ITerminalService { private _hostActiveTerminals: Map<ITerminalInstanceHost, ITerminalInstance | undefined> = new Map(); private _isShuttingDown: boolean; - private _ifNoProfilesTryAgain: boolean = true; private _backgroundedTerminalInstances: ITerminalInstance[] = []; private _backgroundedTerminalDisposables: Map<number, IDisposable[]> = new Map(); private _findState: FindReplaceState; - private readonly _profileProviders: Map</*ext id*/string, Map</*provider id*/string, ITerminalProfileProvider>> = new Map(); private _linkProviders: Set<ITerminalExternalLinkProvider> = new Set(); private _linkProviderDisposables: Map<ITerminalExternalLinkProvider, IDisposable[]> = new Map(); private _processSupportContextKey: IContextKey<boolean>; - private _webExtensionContributedProfileContextKey: IContextKey<boolean>; + private _terminalHasBeenCreated: IContextKey<boolean>; private readonly _localTerminalService?: ILocalTerminalService; private readonly _primaryOffProcessTerminalService?: IOffProcessTerminalService; - private _defaultProfileName?: string; - private _profilesReadyBarrier: AutoOpenBarrier; - private _availableProfiles: ITerminalProfile[] | undefined; - private _contributedProfiles: IExtensionTerminalProfile[] = []; private _configHelper: TerminalConfigHelper; private _remoteTerminalsInitPromise: Promise<void> | undefined; private _localTerminalsInitPromise: Promise<void> | undefined; @@ -84,14 +74,7 @@ export class TerminalService implements ITerminalService { get isProcessSupportRegistered(): boolean { return !!this._processSupportContextKey.get(); } get connectionState(): TerminalConnectionState { return this._connectionState; } - get profilesReady(): Promise<void> { return this._profilesReadyBarrier.wait().then(() => { }); } - get availableProfiles(): ITerminalProfile[] { - this._refreshAvailableProfiles(); - return this._availableProfiles || []; - } - get contributedProfiles(): IExtensionTerminalProfile[] { - return this._contributedProfiles || []; - } + get configHelper(): ITerminalConfigHelper { return this._configHelper; } get instances(): ITerminalInstance[] { return this._terminalGroupService.instances.concat(this._terminalEditorService.instances); @@ -153,8 +136,6 @@ export class TerminalService implements ITerminalService { get onDidRegisterProcessSupport(): Event<void> { return this._onDidRegisterProcessSupport.event; } private readonly _onDidChangeConnectionState = new Emitter<void>(); get onDidChangeConnectionState(): Event<void> { return this._onDidChangeConnectionState.event; } - private readonly _onDidChangeAvailableProfiles = new Emitter<ITerminalProfile[]>(); - get onDidChangeAvailableProfiles(): Event<ITerminalProfile[]> { return this._onDidChangeAvailableProfiles.event; } constructor( @IContextKeyService private _contextKeyService: IContextKeyService, @@ -169,15 +150,15 @@ export class TerminalService implements ITerminalService { @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @IRemoteTerminalService private readonly _remoteTerminalService: IRemoteTerminalService, @ITelemetryService private readonly _telemetryService: ITelemetryService, - @ITerminalContributionService private readonly _terminalContributionService: ITerminalContributionService, @ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService, @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, @ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService, @IEditorResolverService editorResolverService: IEditorResolverService, @IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService, + @IThemeService private readonly _themeService: IThemeService, + @ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService, @IExtensionService private readonly _extensionService: IExtensionService, @INotificationService private readonly _notificationService: INotificationService, - @IThemeService private readonly _themeService: IThemeService, @optional(ILocalTerminalService) localTerminalService: ILocalTerminalService ) { this._localTerminalService = localTerminalService; @@ -216,6 +197,10 @@ export class TerminalService implements ITerminalService { } }; }); + // the below avoids having to poll routinely. + // we update detected profiles when an instance is created so that, + // for example, we detect if you've installed a pwsh + this.onDidCreateInstance(() => this._terminalProfileService.refreshAvailableProfiles()); this._forwardInstanceHostEvents(this._terminalGroupService); this._forwardInstanceHostEvents(this._terminalEditorService); @@ -225,15 +210,6 @@ export class TerminalService implements ITerminalService { this._onDidCreateInstance.fire(instance); }); - // the below avoids having to poll routinely. - // we update detected profiles when an instance is created so that, - // for example, we detect if you've installed a pwsh - this.onDidCreateInstance(() => this._refreshAvailableProfiles()); - - // in web, we don't want to show the dropdown unless there's a web extension - // that contributes a profile - this._extensionService.onDidChangeExtensions(() => this._refreshAvailableProfiles()); - this.onDidReceiveInstanceLinks(instance => this._setInstanceLinkProviders(instance)); // Hide the panel if there are no more instances, provided that VS Code is not shutting @@ -248,21 +224,11 @@ export class TerminalService implements ITerminalService { this._handleInstanceContextKeys(); this._processSupportContextKey = TerminalContextKeys.processSupported.bindTo(this._contextKeyService); this._processSupportContextKey.set(!isWeb || this._remoteAgentService.getConnection() !== null); - this._webExtensionContributedProfileContextKey = TerminalContextKeys.webExtensionContributedProfile.bindTo(this._contextKeyService); this._terminalHasBeenCreated = TerminalContextKeys.terminalHasBeenCreated.bindTo(this._contextKeyService); lifecycleService.onBeforeShutdown(async e => e.veto(this._onBeforeShutdown(e.reason), 'veto.terminal')); lifecycleService.onWillShutdown(e => this._onWillShutdown(e)); - this._configurationService.onDidChangeConfiguration(async e => { - const platformKey = await this._getPlatformKey(); - if (e.affectsConfiguration(TerminalSettingPrefix.DefaultProfile + platformKey) || - e.affectsConfiguration(TerminalSettingPrefix.Profiles + platformKey) || - e.affectsConfiguration(TerminalSettingId.UseWslProfiles)) { - await this._refreshAvailableProfiles(); - } - }); - // Register a resource formatter for terminal URIs labelService.registerFormatter({ scheme: Schemas.vscodeTerminal, @@ -307,12 +273,6 @@ export class TerminalService implements ITerminalService { } }); - // Wait up to 5 seconds for profiles to be ready so it's assured that we know the actual - // default terminal before launching the first terminal. This isn't expected to ever take - // this long. - this._profilesReadyBarrier = new AutoOpenBarrier(5000); - this._refreshAvailableProfiles(); - // Create async as the class depends on `this` timeout(0).then(() => this._instantiationService.createInstance(TerminalEditorStyle, document.head)); } @@ -362,6 +322,23 @@ export class TerminalService implements ITerminalService { } } + async createContributedTerminalProfile(extensionIdentifier: string, id: string, options: ICreateContributedTerminalProfileOptions): Promise<void> { + await this._extensionService.activateByEvent(`onTerminalProfile:${id}`); + + const profileProvider = this._terminalProfileService.getContributedProfileProvider(extensionIdentifier, id); + if (!profileProvider) { + this._notificationService.error(`No terminal profile provider registered for id "${id}"`); + return; + } + try { + await profileProvider.createContributedTerminalProfile(options); + this._terminalGroupService.setActiveInstanceByIndex(this._terminalGroupService.instances.length - 1); + await this._terminalGroupService.activeInstance?.focusWhenReady(); + } catch (e) { + this._notificationService.error(e.message); + } + } + async safeDisposeTerminal(instance: ITerminalInstance): Promise<void> { // Confirm on kill in the editor is handled by the editor input if (instance.target !== TerminalLocation.Editor && @@ -435,7 +412,7 @@ export class TerminalService implements ITerminalService { } } else { // add split terminals to this group - await this.createTerminal({ config: { attachPersistentProcess: terminalLayout.terminal! }, location: { parentTerminal: terminalInstance } }); + terminalInstance = await this.createTerminal({ config: { attachPersistentProcess: terminalLayout.terminal! }, location: { parentTerminal: terminalInstance } }); } } const activeInstance = this.instances.find(t => { @@ -477,18 +454,18 @@ export class TerminalService implements ITerminalService { return this.activeInstance || this.createTerminal(); } - async setEditable(instance: ITerminalInstance, data?: IEditableData | null): Promise<void> { + setEditable(instance: ITerminalInstance, data?: IEditableData | null): void { if (!data) { this._editable = undefined; } else { this._editable = { instance: instance, data }; } const pane = this._viewsService.getActiveViewWithId<TerminalViewPane>(TERMINAL_VIEW_ID); - const isEditing = this._isEditable(instance); + const isEditing = this.isEditable(instance); pane?.terminalTabbedView?.setEditable(isEditing); } - private _isEditable(instance: ITerminalInstance | undefined): boolean { + isEditable(instance: ITerminalInstance | undefined): boolean { return !!this._editable && (this._editable.instance === instance || !instance); } @@ -503,70 +480,6 @@ export class TerminalService implements ITerminalService { }); } - @throttle(2000) - private _refreshAvailableProfiles(): void { - this._refreshAvailableProfilesNow(); - } - - private async _refreshAvailableProfilesNow(): Promise<void> { - const platformKey = await this._getPlatformKey(); - const profiles = await this._detectProfiles(); - const profilesChanged = !equals(profiles, this._availableProfiles); - const excludedContributedProfiles: string[] = []; - const configProfiles: { [key: string]: any } = this._configurationService.getValue(TerminalSettingPrefix.Profiles + platformKey); - for (const [profileName, value] of Object.entries(configProfiles)) { - if (value === null) { - excludedContributedProfiles.push(profileName); - } - } - const filteredContributedProfiles = Array.from(this._terminalContributionService.terminalProfiles.filter(p => !excludedContributedProfiles.includes(p.title))); - const contributedProfilesChanged = !equals(filteredContributedProfiles, this._contributedProfiles); - - if (profiles.length === 0 && this._ifNoProfilesTryAgain) { - // available profiles get updated when a terminal is created - // or relevant config changes. - // if there are no profiles, we want to refresh them again - // since terminal creation can't happen in this case and users - // might not think to try changing the config - this._ifNoProfilesTryAgain = false; - await this._refreshAvailableProfilesNow(); - } - if (profilesChanged || contributedProfilesChanged) { - this._availableProfiles = profiles; - this._contributedProfiles = filteredContributedProfiles; - this._onDidChangeAvailableProfiles.fire(this._availableProfiles); - this._profilesReadyBarrier.open(); - this._updateWebContextKey(); - await this._refreshPlatformConfig(profiles); - } - } - - private _updateWebContextKey(): void { - this._webExtensionContributedProfileContextKey.set(isWeb && this._contributedProfiles.length > 0); - } - - private async _refreshPlatformConfig(profiles: ITerminalProfile[]) { - const env = await this._remoteAgentService.getEnvironment(); - registerTerminalDefaultProfileConfiguration({ os: env?.os || OS, profiles }, this._contributedProfiles); - refreshTerminalActions(profiles); - } - - private async _detectProfiles(includeDetectedProfiles?: boolean): Promise<ITerminalProfile[]> { - if (!this._primaryOffProcessTerminalService) { - return this._availableProfiles || []; - } - const platform = await this._getPlatformKey(); - this._defaultProfileName = this._configurationService.getValue(`${TerminalSettingPrefix.DefaultProfile}${platform}`); - return this._primaryOffProcessTerminalService?.getProfiles(this._configurationService.getValue(`${TerminalSettingPrefix.Profiles}${platform}`), this._defaultProfileName, includeDetectedProfiles); - } - - getDefaultProfileName(): string { - if (!this._defaultProfileName) { - throw new Error('no default profile'); - } - return this._defaultProfileName; - } - private _onBeforeShutdown(reason: ShutdownReason): boolean | Promise<boolean> { // Never veto on web as this would block all windows from being closed. This disables // process revive as we can't handle it on shutdown. @@ -885,16 +798,6 @@ export class TerminalService implements ITerminalService { }; } - registerTerminalProfileProvider(extensionIdentifierenfifier: string, id: string, profileProvider: ITerminalProfileProvider): IDisposable { - let extMap = this._profileProviders.get(extensionIdentifierenfifier); - if (!extMap) { - extMap = new Map(); - this._profileProviders.set(extensionIdentifierenfifier, extMap); - } - extMap.set(id, profileProvider); - return toDisposable(() => this._profileProviders.delete(id)); - } - private _setInstanceLinkProviders(instance: ITerminalInstance): void { for (const linkProvider of this._linkProviders) { const disposables = this._linkProviderDisposables.get(linkProvider); @@ -903,7 +806,6 @@ export class TerminalService implements ITerminalService { } } - // TODO: Remove this, it should live in group/editor servioce private _getIndexFromId(terminalId: number): number { let terminalIndex = -1; @@ -943,7 +845,7 @@ export class TerminalService implements ITerminalService { async showProfileQuickPick(type: 'setDefault' | 'createInstance', cwd?: string | URI): Promise<ITerminalInstance | undefined> { let keyMods: IKeyMods | undefined; - const profiles = await this._detectProfiles(true); + const profiles = this._terminalProfileService.availableProfiles; const platformKey = await this._getPlatformKey(); const profilesKey = `${TerminalSettingPrefix.Profiles}${platformKey}`; const defaultProfileKey = `${TerminalSettingPrefix.DefaultProfile}${platformKey}`; @@ -995,7 +897,7 @@ export class TerminalService implements ITerminalService { quickPickItems.push({ type: 'separator', label: nls.localize('ICreateContributedTerminalProfileOptions', "contributed") }); const contributedProfiles: IProfileQuickPickItem[] = []; - for (const contributed of this.contributedProfiles) { + for (const contributed of this._terminalProfileService.contributedProfiles) { if (typeof contributed.icon === 'string' && contributed.icon.startsWith('$(')) { contributed.icon = contributed.icon.substring(2, contributed.icon.length - 1); } @@ -1044,7 +946,7 @@ export class TerminalService implements ITerminalService { let instance; if ('id' in value.profile) { - await this._createContributedTerminalProfile(value.profile.extensionIdentifier, value.profile.id, { + await this.createContributedTerminalProfile(value.profile.extensionIdentifier, value.profile.id, { icon: value.profile.icon, color: value.profile.color, location: !!(keyMods?.alt && activeInstance) ? { splitActiveTerminal: true } : this.defaultLocation @@ -1071,7 +973,7 @@ export class TerminalService implements ITerminalService { // extension contributed profile await this._configurationService.updateValue(defaultProfileKey, value.profile.title, ConfigurationTarget.USER); - this._registerContributedProfile(value.profile.extensionIdentifier, value.profile.id, value.profile.title, { + this._terminalProfileService.registerContributedProfile(value.profile.extensionIdentifier, value.profile.id, value.profile.title, { color: value.profile.color, icon: value.profile.icon }); @@ -1127,41 +1029,6 @@ export class TerminalService implements ITerminalService { return instance?.target === TerminalLocation.Editor ? this._terminalEditorService : this._terminalGroupService; } - private async _createContributedTerminalProfile(extensionIdentifier: string, id: string, options: ICreateContributedTerminalProfileOptions): Promise<void> { - await this._extensionService.activateByEvent(`onTerminalProfile:${id}`); - const extMap = this._profileProviders.get(extensionIdentifier); - const profileProvider = extMap?.get(id); - if (!profileProvider) { - this._notificationService.error(`No terminal profile provider registered for id "${id}"`); - return; - } - try { - await profileProvider.createContributedTerminalProfile(options); - this._terminalGroupService.setActiveInstanceByIndex(this.instances.length - 1); - await this.activeInstance?.focusWhenReady(); - } catch (e) { - this._notificationService.error(e.message); - } - } - - private async _registerContributedProfile(extensionIdentifier: string, id: string, title: string, options: ICreateContributedTerminalProfileOptions): Promise<void> { - const platformKey = await this._getPlatformKey(); - const profilesConfig = await this._configurationService.getValue(`${TerminalSettingPrefix.Profiles}${platformKey}`); - if (typeof profilesConfig === 'object') { - const newProfile: IExtensionTerminalProfile = { - extensionIdentifier: extensionIdentifier, - icon: options.icon, - id, - title: title, - color: options.color - }; - - (profilesConfig as { [key: string]: ITerminalProfileObject })[title] = newProfile; - } - await this._configurationService.updateValue(`${TerminalSettingPrefix.Profiles}${platformKey}`, profilesConfig, ConfigurationTarget.USER); - return; - } - private _createProfileQuickPickItem(profile: ITerminalProfile): IProfileQuickPickItem { const buttons: IQuickInputButton[] = [{ iconClass: ThemeIcon.asClassName(configureTerminalProfileIcon), @@ -1231,32 +1098,19 @@ export class TerminalService implements ITerminalService { return {}; } - private async _getContributedDefaultProfile(shellLaunchConfig: IShellLaunchConfig): Promise<IExtensionTerminalProfile | undefined> { - // prevents recursion with the MainThreadTerminalService call to create terminal - // and defers to the provided launch config when an executable is provided - if (shellLaunchConfig && !shellLaunchConfig.extHostTerminalId && !('executable' in shellLaunchConfig)) { - const key = await this._getPlatformKey(); - const defaultProfileName = this._configurationService.getValue(`${TerminalSettingPrefix.DefaultProfile}${key}`); - const contributedDefaultProfile = this._terminalContributionService.terminalProfiles.find(p => p.title === defaultProfileName); - return contributedDefaultProfile; - } - return undefined; - } - - async createTerminal(options?: ICreateTerminalOptions): Promise<ITerminalInstance> { // Await the initialization of available profiles as long as this is not a pty terminal or a // local terminal in a remote workspace as profile won't be used in those cases and these // terminals need to be launched before remote connections are established. - if (!this._availableProfiles) { + if (!this._terminalProfileService.availableProfiles) { const isPtyTerminal = options?.config && 'customPtyImplementation' in options.config; const isLocalInRemoteTerminal = this._remoteAgentService.getConnection() && URI.isUri(options?.cwd) && options?.cwd.scheme === Schemas.vscodeFileResource; if (!isPtyTerminal && !isLocalInRemoteTerminal) { - await this._refreshAvailableProfilesNow(); + await this._terminalProfileService.refreshAvailableProfiles(); } } - const config = options?.config || this._availableProfiles?.find(p => p.profileName === this._defaultProfileName); + const config = options?.config || this._terminalProfileService.availableProfiles?.find(p => p.profileName === this._terminalProfileService.getDefaultProfileName()); const shellLaunchConfig = config && 'extensionIdentifier' in config ? {} : this._convertProfileToShellLaunchConfig(config || {}); // Get the contributed profile if it was provided @@ -1264,7 +1118,7 @@ export class TerminalService implements ITerminalService { // Get the default profile as a contributed profile if it exists if (!contributedProfile && (!options || !options.config)) { - contributedProfile = await this._getContributedDefaultProfile(shellLaunchConfig); + contributedProfile = await this._terminalProfileService.getContributedDefaultProfile(shellLaunchConfig); } // Launch the contributed profile @@ -1277,7 +1131,7 @@ export class TerminalService implements ITerminalService { } else { location = typeof options?.location === 'object' && 'viewColumn' in options.location ? options.location : resolvedLocation; } - await this._createContributedTerminalProfile(contributedProfile.extensionIdentifier, contributedProfile.id, { + await this.createContributedTerminalProfile(contributedProfile.extensionIdentifier, contributedProfile.id, { icon: contributedProfile.icon, color: contributedProfile.color, location @@ -1371,10 +1225,6 @@ export class TerminalService implements ITerminalService { } private _getSplitParent(location?: ITerminalLocationOptions): ITerminalInstance | undefined { - if (this._connectionState === TerminalConnectionState.Connecting && this.activeInstance) { - const group = this._terminalGroupService.getGroupForInstance(this.activeInstance); - return group?.terminalInstances[group.terminalInstances.length - 1]; - } if (location && typeof location === 'object' && 'parentTerminal' in location) { return location.parentTerminal; } else if (location && typeof location === 'object' && 'splitActiveTerminal' in location) { @@ -1446,6 +1296,7 @@ class TerminalEditorStyle extends Themable { container: HTMLElement, @ITerminalService private readonly _terminalService: ITerminalService, @IThemeService private readonly _themeService: IThemeService, + @ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService ) { super(_themeService); this._registerListeners(); @@ -1459,7 +1310,7 @@ class TerminalEditorStyle extends Themable { this._register(this._terminalService.onDidChangeInstanceIcon(() => this.updateStyles())); this._register(this._terminalService.onDidChangeInstanceColor(() => this.updateStyles())); this._register(this._terminalService.onDidCreateInstance(() => this.updateStyles())); - this._register(this._terminalService.onDidChangeAvailableProfiles(() => this.updateStyles())); + this._register(this._terminalProfileService.onDidChangeAvailableProfiles(() => this.updateStyles())); } override updateStyles(): void { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts index 4dfaeb471f3..adae67849dc 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts @@ -439,7 +439,7 @@ export class TerminalTabbedView extends Disposable { if (!isEditing) { this._tabList.domFocus(); } - return this._tabList.refresh(); + this._tabList.refresh(false); } focusTabs(): void { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index a24777322bc..a2b2a1162af 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -192,7 +192,11 @@ export class TerminalTabList extends WorkbenchList<ITerminalInstance> { return this._configurationService.getValue<'singleClick' | 'doubleClick'>(TerminalSettingId.TabsFocusMode); } - refresh(): void { + refresh(cancelEditing: boolean = true): void { + if (cancelEditing && this._terminalService.isEditable(undefined)) { + this.domFocus(); + } + this.splice(0, this.length, this._terminalGroupService.instances.slice()); } @@ -324,12 +328,12 @@ class TerminalTabsRenderer implements IListRenderer<ITerminalInstance, ITerminal template.actionBar.clear(); } - if (!template.elementDispoables) { - template.elementDispoables = new DisposableStore(); + if (!template.elementDisposables) { + template.elementDisposables = new DisposableStore(); } // Kill terminal on middle click - template.elementDispoables.add(DOM.addDisposableListener(template.element, DOM.EventType.AUXCLICK, e => { + template.elementDisposables.add(DOM.addDisposableListener(template.element, DOM.EventType.AUXCLICK, e => { e.stopImmediatePropagation(); if (e.button === 1/*middle*/) { this._terminalService.safeDisposeTerminal(instance); @@ -364,14 +368,13 @@ class TerminalTabsRenderer implements IListRenderer<ITerminalInstance, ITerminal const editableData = this._terminalService.getEditableData(instance); template.label.element.classList.toggle('editable-tab', !!editableData); if (editableData) { - this._renderInputBox(template.label.element.querySelector('.monaco-icon-label-container')!, instance, editableData); + template.elementDisposables.add(this._renderInputBox(template.label.element.querySelector('.monaco-icon-label-container')!, instance, editableData)); template.actionBar.clear(); } } private _renderInputBox(container: HTMLElement, instance: ITerminalInstance, editableData: IEditableData): IDisposable { - const label = this._labels.create(container); const value = instance.title || ''; const inputBox = new InputBox(container, this._contextViewService, { @@ -439,7 +442,6 @@ class TerminalTabsRenderer implements IListRenderer<ITerminalInstance, ITerminal DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => { done(inputBox.isInputValid(), true); }), - label, styler ]; @@ -449,11 +451,14 @@ class TerminalTabsRenderer implements IListRenderer<ITerminalInstance, ITerminal } disposeElement(instance: ITerminalInstance, index: number, templateData: ITerminalTabEntryTemplate): void { - templateData.elementDispoables?.dispose(); - templateData.elementDispoables = undefined; + templateData.elementDisposables?.dispose(); + templateData.elementDisposables = undefined; } disposeTemplate(templateData: ITerminalTabEntryTemplate): void { + templateData.elementDisposables?.dispose(); + templateData.elementDisposables = undefined; + templateData.label.dispose(); } fillActionBar(instance: ITerminalInstance, template: ITerminalTabEntryTemplate): void { @@ -496,7 +501,7 @@ interface ITerminalTabEntryTemplate { context: { hoverActions?: IHoverAction[]; }; - elementDispoables?: DisposableStore; + elementDisposables?: DisposableStore; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts index 8bdf4fcf67e..689c92ae0a0 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts @@ -11,7 +11,7 @@ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; -import { XTermAttributes, XTermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; +import { XtermAttributes, IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; import { DEFAULT_LOCAL_ECHO_EXCLUDE, IBeforeProcessDataEvent, ITerminalConfiguration, ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; import type { IBuffer, IBufferCell, IDisposable, ITerminalAddon, Terminal } from 'xterm'; @@ -44,7 +44,7 @@ const statsToggleOffThreshold = 0.5; // if latency is less than `threshold * thi */ const PREDICTION_OMIT_RE = /^(\x1b\[(\??25[hl]|\??[0-9;]+n))+/; -const core = (terminal: Terminal): XTermCore => (terminal as any)._core; +const core = (terminal: Terminal): IXtermCore => (terminal as any)._core; const flushOutput = (terminal: Terminal) => { // TODO: Flushing output is not possible anymore without async }; @@ -1032,7 +1032,7 @@ export class PredictionTimeline { /** * Gets the escape sequence args to restore state/appearence in the cell. */ -const attributesToArgs = (cell: XTermAttributes) => { +const attributesToArgs = (cell: XtermAttributes) => { if (cell.isAttributeDefault()) { return [0]; } const args = []; @@ -1058,7 +1058,7 @@ const attributesToArgs = (cell: XTermAttributes) => { /** * Gets the escape sequence to restore state/appearence in the cell. */ -const attributesToSeq = (cell: XTermAttributes) => `${CSI}${attributesToArgs(cell).join(';')}m`; +const attributesToSeq = (cell: XtermAttributes) => `${CSI}${attributesToArgs(cell).join(';')}m`; const arrayHasPrefixAt = <T>(a: ReadonlyArray<T>, ai: number, b: ReadonlyArray<T>) => { if (a.length - ai > b.length) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 567d475e245..75d335bae1b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -22,7 +22,7 @@ import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND, EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { IMenu, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; -import { ITerminalProfileResolverService, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalProfileResolverService, ITerminalProfileService, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalSettingId, ITerminalProfile, TerminalLocation } from 'vs/platform/terminal/common/terminal'; import { ActionViewItem, SelectActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { attachSelectBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; @@ -74,6 +74,7 @@ export class TerminalViewPane extends ViewPane { @IOpenerService openerService: IOpenerService, @IMenuService private readonly _menuService: IMenuService, @ICommandService private readonly _commandService: ICommandService, + @ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService, @ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService ) { super(options, keybindingService, _contextMenuService, configurationService, _contextKeyService, viewDescriptorService, _instantiationService, openerService, themeService, telemetryService); @@ -98,7 +99,7 @@ export class TerminalViewPane extends ViewPane { }); this._dropdownMenu = this._register(this._menuService.createMenu(MenuId.TerminalNewDropdownContext, this._contextKeyService)); this._singleTabMenu = this._register(this._menuService.createMenu(MenuId.TerminalInlineTabContext, this._contextKeyService)); - this._register(this._terminalService.onDidChangeAvailableProfiles(profiles => this._updateTabActionBar(profiles))); + this._register(this._terminalProfileService.onDidChangeAvailableProfiles(profiles => this._updateTabActionBar(profiles))); } override renderBody(container: HTMLElement): void { @@ -202,9 +203,9 @@ export class TerminalViewPane extends ViewPane { this._tabButtons.dispose(); } - const actions = getTerminalActionBarArgs(TerminalLocation.Panel, this._terminalService.availableProfiles, this._getDefaultProfileName(), this._terminalService.contributedProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); + const actions = getTerminalActionBarArgs(TerminalLocation.Panel, this._terminalProfileService.availableProfiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); this._tabButtons = new DropdownWithPrimaryActionViewItem(actions.primaryAction, actions.dropdownAction, actions.dropdownMenuActions, actions.className, this._contextMenuService, {}, this._keybindingService, this._notificationService, this._contextKeyService); - this._updateTabActionBar(this._terminalService.availableProfiles); + this._updateTabActionBar(this._terminalProfileService.availableProfiles); return this._tabButtons; } } @@ -214,7 +215,7 @@ export class TerminalViewPane extends ViewPane { private _getDefaultProfileName(): string { let defaultProfileName; try { - defaultProfileName = this._terminalService.getDefaultProfileName(); + defaultProfileName = this._terminalProfileService.getDefaultProfileName(); } catch (e) { defaultProfileName = this._terminalProfileResolverService.defaultProfileName; } @@ -226,7 +227,7 @@ export class TerminalViewPane extends ViewPane { } private _updateTabActionBar(profiles: ITerminalProfile[]): void { - const actions = getTerminalActionBarArgs(TerminalLocation.Panel, profiles, this._getDefaultProfileName(), this._terminalService.contributedProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); + const actions = getTerminalActionBarArgs(TerminalLocation.Panel, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); this._tabButtons?.update(actions.dropdownAction, actions.dropdownMenuActions); } @@ -264,6 +265,7 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = const sidebarBackgroundColor = theme.getColor(TERMINAL_BACKGROUND_COLOR) || theme.getColor(SIDE_BAR_BACKGROUND); collector.addRule(`.monaco-workbench .part.sidebar .pane-body.integrated-terminal .terminal-outer-container { background-color: ${sidebarBackgroundColor ? sidebarBackgroundColor.toString() : ''}; }`); + collector.addRule(`.monaco-workbench .part.auxiliarybar .pane-body.integrated-terminal .terminal-outer-container { background-color: ${sidebarBackgroundColor ? sidebarBackgroundColor.toString() : ''}; }`); const borderColor = theme.getColor(TERMINAL_BORDER_COLOR); if (borderColor) { @@ -289,7 +291,8 @@ class SwitchTerminalActionViewItem extends SelectActionViewItem { @ITerminalService private readonly _terminalService: ITerminalService, @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, @IThemeService private readonly _themeService: IThemeService, - @IContextViewService contextViewService: IContextViewService + @IContextViewService contextViewService: IContextViewService, + @ITerminalProfileService terminalProfileService: ITerminalProfileService ) { super(null, action, getTerminalSelectOpenItems(_terminalService, _terminalGroupService), _terminalGroupService.activeGroupIndex, contextViewService, { ariaLabel: nls.localize('terminals', 'Open Terminals.'), optionsAsChildren: true }); this._register(_terminalService.onDidChangeInstances(() => this._updateItems(), this)); @@ -298,7 +301,7 @@ class SwitchTerminalActionViewItem extends SelectActionViewItem { this._register(_terminalService.onDidChangeInstanceTitle(() => this._updateItems(), this)); this._register(_terminalGroupService.onDidChangeGroups(() => this._updateItems(), this)); this._register(_terminalService.onDidChangeConnectionState(() => this._updateItems(), this)); - this._register(_terminalService.onDidChangeAvailableProfiles(() => this._updateItems(), this)); + this._register(terminalProfileService.onDidChangeAvailableProfiles(() => this._updateItems(), this)); this._register(_terminalService.onDidChangeInstancePrimaryStatus(() => this._updateItems(), this)); this._register(attachSelectBoxStyler(this.selectBox, this._themeService)); } diff --git a/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts b/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts index 35d8ed4ef55..b700ee167fa 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts @@ -7,9 +7,9 @@ import { IBufferCell } from 'xterm'; -export type XTermAttributes = Omit<IBufferCell, 'getWidth' | 'getChars' | 'getCode'> & { clone?(): XTermAttributes }; +export type XtermAttributes = Omit<IBufferCell, 'getWidth' | 'getChars' | 'getCode'> & { clone?(): XtermAttributes }; -export interface XTermCore { +export interface IXtermCore { viewport?: { _innerRefresh(): void; }; @@ -25,7 +25,7 @@ export interface XTermCore { }; _inputHandler: { - _curAttrData: XTermAttributes; + _curAttrData: XtermAttributes; }; _renderService: { @@ -34,7 +34,7 @@ export interface XTermCore { actualCellHeight: number; }, _renderer: { - _renderLayers: any[]; + _renderLayers?: any[]; }; _onIntersectionChange: any; }; diff --git a/src/vs/workbench/contrib/terminal/browser/addons/commandTrackerAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/commandTrackerAddon.ts similarity index 100% rename from src/vs/workbench/contrib/terminal/browser/addons/commandTrackerAddon.ts rename to src/vs/workbench/contrib/terminal/browser/xterm/commandTrackerAddon.ts diff --git a/src/vs/workbench/contrib/terminal/browser/addons/lineDataEventAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/lineDataEventAddon.ts similarity index 98% rename from src/vs/workbench/contrib/terminal/browser/addons/lineDataEventAddon.ts rename to src/vs/workbench/contrib/terminal/browser/xterm/lineDataEventAddon.ts index c0bcedaae03..9d16ca4f973 100644 --- a/src/vs/workbench/contrib/terminal/browser/addons/lineDataEventAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/lineDataEventAddon.ts @@ -22,13 +22,13 @@ export class LineDataEventAddon extends Disposable implements ITerminalAddon { activate(xterm: XTermTerminal) { this._xterm = xterm; // Fire onLineData when a line feed occurs, taking into account wrapped lines - xterm.onLineFeed(() => { + this._register(xterm.onLineFeed(() => { const buffer = xterm.buffer; const newLine = buffer.active.getLine(buffer.active.baseY + buffer.active.cursorY); if (newLine && !newLine.isWrapped) { this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY - 1); } - }); + })); // Fire onLineData when disposing object to flush last line this._register(toDisposable(() => { diff --git a/src/vs/workbench/contrib/terminal/browser/addons/navigationModeAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/navigationModeAddon.ts similarity index 100% rename from src/vs/workbench/contrib/terminal/browser/addons/navigationModeAddon.ts rename to src/vs/workbench/contrib/terminal/browser/xterm/navigationModeAddon.ts diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts new file mode 100644 index 00000000000..dfc69cec4fd --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -0,0 +1,453 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { ITheme, RendererType, Terminal as RawXtermTerminal } from 'xterm'; +import type { ISearchOptions, SearchAddon as SearchAddonType } from 'xterm-addon-search'; +import type { Unicode11Addon as Unicode11AddonType } from 'xterm-addon-unicode11'; +import type { WebglAddon as WebglAddonType } from 'xterm-addon-webgl'; +import { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; +import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { TerminalLocation, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; +import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; +import { editorBackground } from 'vs/platform/theme/common/colorRegistry'; +import { ansiColorIdentifiers, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_BACKGROUND_COLOR, TERMINAL_CURSOR_FOREGROUND_COLOR, TERMINAL_FOREGROUND_COLOR, TERMINAL_SELECTION_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; +import { ICommandTracker, ITerminalFont, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; +import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { Color } from 'vs/base/common/color'; +import { isSafari } from 'vs/base/browser/browser'; +import { IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys'; +import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; +import { CommandTrackerAddon } from 'vs/workbench/contrib/terminal/browser/xterm/commandTrackerAddon'; +import { localize } from 'vs/nls'; + +// How long in milliseconds should an average frame take to render for a notification to appear +// which suggests the fallback DOM-based renderer +const SLOW_CANVAS_RENDER_THRESHOLD = 50; +const NUMBER_OF_FRAMES_TO_MEASURE = 20; + +let SearchAddon: typeof SearchAddonType; +let Unicode11Addon: typeof Unicode11AddonType; +let WebglAddon: typeof WebglAddonType; + +/** + * Wraps the xterm object with additional functionality. Interaction with the backing process is out + * of the scope of this class. + */ +export class XtermTerminal extends DisposableStore implements IXtermTerminal { + /** The raw xterm.js instance */ + readonly raw: RawXtermTerminal; + target?: TerminalLocation; + + private _core: IXtermCore; + private static _suggestedRendererType: 'canvas' | 'dom' | undefined = undefined; + private _container?: HTMLElement; + + // Always on addons + private _commandTrackerAddon: CommandTrackerAddon; + + // Lazily loaded addons + private _searchAddon?: SearchAddonType; + + // Optional addons + private _unicode11Addon?: Unicode11AddonType; + private _webglAddon?: WebglAddonType; + + get commandTracker(): ICommandTracker { return this._commandTrackerAddon; } + + /** + * @param xtermCtor The xterm.js constructor, this is passed in so it can be fetched lazily + * outside of this class such that {@link raw} is not nullable. + */ + constructor( + xtermCtor: typeof RawXtermTerminal, + private readonly _configHelper: TerminalConfigHelper, + cols: number, + rows: number, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @ILogService private readonly _logService: ILogService, + @INotificationService private readonly _notificationService: INotificationService, + @IStorageService private readonly _storageService: IStorageService, + @IThemeService private readonly _themeService: IThemeService, + @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService + ) { + super(); + + const font = this._configHelper.getFont(undefined, true); + const config = this._configHelper.config; + const editorOptions = this._configurationService.getValue<IEditorOptions>('editor'); + + this.raw = this.add(new xtermCtor({ + cols, + rows, + altClickMovesCursor: config.altClickMovesCursor && editorOptions.multiCursorModifier === 'alt', + scrollback: config.scrollback, + theme: this._getXtermTheme(), + drawBoldTextInBrightColors: config.drawBoldTextInBrightColors, + fontFamily: font.fontFamily, + fontWeight: config.fontWeight, + fontWeightBold: config.fontWeightBold, + fontSize: font.fontSize, + letterSpacing: font.letterSpacing, + lineHeight: font.lineHeight, + minimumContrastRatio: config.minimumContrastRatio, + cursorBlink: config.cursorBlinking, + cursorStyle: config.cursorStyle === 'line' ? 'bar' : config.cursorStyle, + cursorWidth: config.cursorWidth, + bellStyle: 'none', + macOptionIsMeta: config.macOptionIsMeta, + macOptionClickForcesSelection: config.macOptionClickForcesSelection, + rightClickSelectsWord: config.rightClickBehavior === 'selectWord', + fastScrollModifier: 'alt', + fastScrollSensitivity: editorOptions.fastScrollSensitivity, + scrollSensitivity: editorOptions.mouseWheelScrollSensitivity, + rendererType: this._getBuiltInXtermRenderer(config.gpuAcceleration, XtermTerminal._suggestedRendererType), + wordSeparator: config.wordSeparators + })); + this._core = (this.raw as any)._core as IXtermCore; + + this.add(this._configurationService.onDidChangeConfiguration(async e => { + if (e.affectsConfiguration(TerminalSettingId.GpuAcceleration)) { + XtermTerminal._suggestedRendererType = undefined; + } + if (e.affectsConfiguration('terminal.integrated') || e.affectsConfiguration('editor.fastScrollSensitivity') || e.affectsConfiguration('editor.mouseWheelScrollSensitivity') || e.affectsConfiguration('editor.multiCursorModifier')) { + this.updateConfig(); + } + if (e.affectsConfiguration(TerminalSettingId.UnicodeVersion)) { + this._updateUnicodeVersion(); + } + })); + this.add(this._themeService.onDidColorThemeChange(theme => this._updateTheme(theme))); + this.add(this._viewDescriptorService.onDidChangeLocation(({ views }) => { + if (views.some(v => v.id === TERMINAL_VIEW_ID)) { + this._updateTheme(); + } + })); + + // Load addons + + this._updateUnicodeVersion(); + + this._commandTrackerAddon = new CommandTrackerAddon(); + this.raw.loadAddon(this._commandTrackerAddon); + + this._getSearchAddonConstructor().then(addonCtor => { + this._searchAddon = new addonCtor(); + this.raw.loadAddon(this._searchAddon); + }); + } + + attachToElement(container: HTMLElement) { + // Update the theme when attaching as the terminal location could have changed + this._updateTheme(); + + if (!this._container) { + this.raw.open(container); + } + this._container = container; + } + + updateConfig(): void { + const config = this._configHelper.config; + this._safeSetOption('altClickMovesCursor', config.altClickMovesCursor); + this._setCursorBlink(config.cursorBlinking); + this._setCursorStyle(config.cursorStyle); + this._setCursorWidth(config.cursorWidth); + this._safeSetOption('scrollback', config.scrollback); + this._safeSetOption('drawBoldTextInBrightColors', config.drawBoldTextInBrightColors); + this._safeSetOption('minimumContrastRatio', config.minimumContrastRatio); + this._safeSetOption('fastScrollSensitivity', config.fastScrollSensitivity); + this._safeSetOption('scrollSensitivity', config.mouseWheelScrollSensitivity); + this._safeSetOption('macOptionIsMeta', config.macOptionIsMeta); + const editorOptions = this._configurationService.getValue<IEditorOptions>('editor'); + this._safeSetOption('altClickMovesCursor', config.altClickMovesCursor && editorOptions.multiCursorModifier === 'alt'); + this._safeSetOption('macOptionClickForcesSelection', config.macOptionClickForcesSelection); + this._safeSetOption('rightClickSelectsWord', config.rightClickBehavior === 'selectWord'); + this._safeSetOption('wordSeparator', config.wordSeparators); + this._safeSetOption('customGlyphs', config.customGlyphs); + if ((!isSafari && config.gpuAcceleration === 'auto' && XtermTerminal._suggestedRendererType === undefined) || config.gpuAcceleration === 'on') { + this._enableWebglRenderer(); + } else { + this._disposeOfWebglRenderer(); + this._safeSetOption('rendererType', this._getBuiltInXtermRenderer(config.gpuAcceleration, XtermTerminal._suggestedRendererType)); + } + } + + forceRedraw() { + this._webglAddon?.clearTextureAtlas(); + this.raw.clearTextureAtlas(); + } + + + forceRefresh() { + this._core.viewport?._innerRefresh(); + } + + forceUnpause() { + // HACK: Force the renderer to unpause by simulating an IntersectionObserver event. + // This is to fix an issue where dragging the windpow to the top of the screen to + // maximize on Windows/Linux would fire an event saying that the terminal was not + // visible. + if (this.raw.getOption('rendererType') === 'canvas') { + this._core._renderService?._onIntersectionChange({ intersectionRatio: 1 }); + // HACK: Force a refresh of the screen to ensure links are refresh corrected. + // This can probably be removed when the above hack is fixed in Chromium. + this.raw.refresh(0, this.raw.rows - 1); + } + } + + findNext(term: string, searchOptions: ISearchOptions): boolean { + if (!this._searchAddon) { + return false; + } + return this._searchAddon.findNext(term, searchOptions); + } + + findPrevious(term: string, searchOptions: ISearchOptions): boolean { + if (!this._searchAddon) { + return false; + } + return this._searchAddon.findPrevious(term, searchOptions); + } + + getFont(): ITerminalFont { + return this._configHelper.getFont(this._core); + } + + scrollDownLine(): void { + this.raw.scrollLines(1); + } + + scrollDownPage(): void { + this.raw.scrollPages(1); + } + + scrollToBottom(): void { + this.raw.scrollToBottom(); + } + + scrollUpLine(): void { + this.raw.scrollLines(-1); + } + + scrollUpPage(): void { + this.raw.scrollPages(-1); + } + + scrollToTop(): void { + this.raw.scrollToTop(); + } + + clearBuffer(): void { + this.raw.clear(); + } + + private _safeSetOption(key: string, value: any): void { + if (this.raw.getOption(key) !== value) { + this.raw.setOption(key, value); + } + } + + private _setCursorBlink(blink: boolean): void { + if (this.raw.getOption('cursorBlink') !== blink) { + this.raw.setOption('cursorBlink', blink); + this.raw.refresh(0, this.raw.rows - 1); + } + } + + private _setCursorStyle(style: string): void { + if (this.raw.getOption('cursorStyle') !== style) { + // 'line' is used instead of bar in VS Code to be consistent with editor.cursorStyle + const xtermOption = style === 'line' ? 'bar' : style; + this.raw.setOption('cursorStyle', xtermOption); + } + } + + private _setCursorWidth(width: number): void { + if (this.raw.getOption('cursorWidth') !== width) { + this.raw.setOption('cursorWidth', width); + } + } + + private _getBuiltInXtermRenderer(gpuAcceleration: string, suggestedRendererType?: string): RendererType { + let rendererType: RendererType = 'canvas'; + if (gpuAcceleration === 'off' || (gpuAcceleration === 'auto' && suggestedRendererType === 'dom')) { + rendererType = 'dom'; + } + return rendererType; + } + + private async _enableWebglRenderer(): Promise<void> { + if (!this.raw.element || this._webglAddon) { + return; + } + const Addon = await this._getWebglAddonConstructor(); + this._webglAddon = new Addon(); + try { + this.raw.loadAddon(this._webglAddon); + this._webglAddon.onContextLoss(() => { + this._logService.info(`Webgl lost context, disposing of webgl renderer`); + this._disposeOfWebglRenderer(); + this._safeSetOption('rendererType', 'dom'); + }); + } catch (e) { + this._logService.warn(`Webgl could not be loaded. Falling back to the canvas renderer type.`, e); + const neverMeasureRenderTime = this._storageService.getBoolean(TerminalStorageKeys.NeverMeasureRenderTime, StorageScope.GLOBAL, false); + // if it's already set to dom, no need to measure render time + if (!neverMeasureRenderTime && this._configHelper.config.gpuAcceleration !== 'off') { + this._measureRenderTime(); + } + this._safeSetOption('rendererType', 'canvas'); + XtermTerminal._suggestedRendererType = 'canvas'; + this._disposeOfWebglRenderer(); + } + } + + protected async _getSearchAddonConstructor(): Promise<typeof SearchAddonType> { + if (!SearchAddon) { + SearchAddon = (await import('xterm-addon-search')).SearchAddon; + } + return SearchAddon; + } + + protected async _getUnicode11Constructor(): Promise<typeof Unicode11AddonType> { + if (!Unicode11Addon) { + Unicode11Addon = (await import('xterm-addon-unicode11')).Unicode11Addon; + } + return Unicode11Addon; + } + + protected async _getWebglAddonConstructor(): Promise<typeof WebglAddonType> { + if (!WebglAddon) { + WebglAddon = (await import('xterm-addon-webgl')).WebglAddon; + } + return WebglAddon; + } + + private _disposeOfWebglRenderer(): void { + try { + this._webglAddon?.dispose(); + } catch { + // ignore + } + this._webglAddon = undefined; + } + + private async _measureRenderTime(): Promise<void> { + const frameTimes: number[] = []; + if (!this._core._renderService?._renderer._renderLayers) { + return; + } + const textRenderLayer = this._core._renderService._renderer._renderLayers[0]; + const originalOnGridChanged = textRenderLayer?.onGridChanged; + const evaluateCanvasRenderer = () => { + // Discard first frame time as it's normal to take longer + frameTimes.shift(); + + const medianTime = frameTimes.sort((a, b) => a - b)[Math.floor(frameTimes.length / 2)]; + if (medianTime > SLOW_CANVAS_RENDER_THRESHOLD) { + if (this._configHelper.config.gpuAcceleration === 'auto') { + XtermTerminal._suggestedRendererType = 'dom'; + this.updateConfig(); + } else { + const promptChoices: IPromptChoice[] = [ + { + label: localize('yes', "Yes"), + run: () => this._configurationService.updateValue(TerminalSettingId.GpuAcceleration, 'off', ConfigurationTarget.USER) + } as IPromptChoice, + { + label: localize('no', "No"), + run: () => { } + } as IPromptChoice, + { + label: localize('dontShowAgain', "Don't Show Again"), + isSecondary: true, + run: () => this._storageService.store(TerminalStorageKeys.NeverMeasureRenderTime, true, StorageScope.GLOBAL, StorageTarget.MACHINE) + } as IPromptChoice + ]; + this._notificationService.prompt( + Severity.Warning, + localize('terminal.slowRendering', 'Terminal GPU acceleration appears to be slow on your computer. Would you like to switch to disable it which may improve performance? [Read more about terminal settings](https://code.visualstudio.com/docs/editor/integrated-terminal#_changing-how-the-terminal-is-rendered).'), + promptChoices + ); + } + } + }; + + textRenderLayer.onGridChanged = (terminal: RawXtermTerminal, firstRow: number, lastRow: number) => { + const startTime = performance.now(); + originalOnGridChanged.call(textRenderLayer, terminal, firstRow, lastRow); + frameTimes.push(performance.now() - startTime); + if (frameTimes.length === NUMBER_OF_FRAMES_TO_MEASURE) { + evaluateCanvasRenderer(); + // Restore original function + textRenderLayer.onGridChanged = originalOnGridChanged; + } + }; + } + + private _getXtermTheme(theme?: IColorTheme): ITheme { + if (!theme) { + theme = this._themeService.getColorTheme(); + } + + const location = this._viewDescriptorService.getViewLocationById(TERMINAL_VIEW_ID)!; + const foregroundColor = theme.getColor(TERMINAL_FOREGROUND_COLOR); + let backgroundColor: Color | undefined; + if (this.target === TerminalLocation.Editor) { + backgroundColor = theme.getColor(TERMINAL_BACKGROUND_COLOR) || theme.getColor(editorBackground); + } else { + backgroundColor = theme.getColor(TERMINAL_BACKGROUND_COLOR) || (location === ViewContainerLocation.Panel ? theme.getColor(PANEL_BACKGROUND) : theme.getColor(SIDE_BAR_BACKGROUND)); + } + const cursorColor = theme.getColor(TERMINAL_CURSOR_FOREGROUND_COLOR) || foregroundColor; + const cursorAccentColor = theme.getColor(TERMINAL_CURSOR_BACKGROUND_COLOR) || backgroundColor; + const selectionColor = theme.getColor(TERMINAL_SELECTION_BACKGROUND_COLOR); + + return { + background: backgroundColor ? backgroundColor.toString() : undefined, + foreground: foregroundColor ? foregroundColor.toString() : undefined, + cursor: cursorColor ? cursorColor.toString() : undefined, + cursorAccent: cursorAccentColor ? cursorAccentColor.toString() : undefined, + selection: selectionColor ? selectionColor.toString() : undefined, + black: theme.getColor(ansiColorIdentifiers[0])?.toString(), + red: theme.getColor(ansiColorIdentifiers[1])?.toString(), + green: theme.getColor(ansiColorIdentifiers[2])?.toString(), + yellow: theme.getColor(ansiColorIdentifiers[3])?.toString(), + blue: theme.getColor(ansiColorIdentifiers[4])?.toString(), + magenta: theme.getColor(ansiColorIdentifiers[5])?.toString(), + cyan: theme.getColor(ansiColorIdentifiers[6])?.toString(), + white: theme.getColor(ansiColorIdentifiers[7])?.toString(), + brightBlack: theme.getColor(ansiColorIdentifiers[8])?.toString(), + brightRed: theme.getColor(ansiColorIdentifiers[9])?.toString(), + brightGreen: theme.getColor(ansiColorIdentifiers[10])?.toString(), + brightYellow: theme.getColor(ansiColorIdentifiers[11])?.toString(), + brightBlue: theme.getColor(ansiColorIdentifiers[12])?.toString(), + brightMagenta: theme.getColor(ansiColorIdentifiers[13])?.toString(), + brightCyan: theme.getColor(ansiColorIdentifiers[14])?.toString(), + brightWhite: theme.getColor(ansiColorIdentifiers[15])?.toString() + }; + } + + private _updateTheme(theme?: IColorTheme): void { + this.raw.setOption('theme', this._getXtermTheme(theme)); + } + + private async _updateUnicodeVersion(): Promise<void> { + if (!this._unicode11Addon && this._configHelper.config.unicodeVersion === '11') { + const Addon = await this._getUnicode11Constructor(); + this._unicode11Addon = new Addon(); + this.raw.loadAddon(this._unicode11Addon); + } + if (this.raw.unicode.activeVersion !== this._configHelper.config.unicodeVersion) { + this.raw.unicode.activeVersion = this._configHelper.config.unicodeVersion; + } + } +} diff --git a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts index e29b58dcecd..a1894b392fc 100644 --- a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts +++ b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts @@ -18,7 +18,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { Schemas } from 'vs/base/common/network'; import { ILabelService } from 'vs/platform/label/common/label'; import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { IProcessDataEvent, IRequestResolveVariablesEvent, IShellLaunchConfig, IShellLaunchConfigDto, ITerminalDimensionsOverride, ITerminalEnvironment, ITerminalLaunchError, ITerminalProfile, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon, IProcessProperty, TerminalShellType, ProcessPropertyType, ProcessCapability, IProcessPropertyMap } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IRequestResolveVariablesEvent, IShellLaunchConfigDto, ITerminalEnvironment, ITerminalLaunchError, ITerminalProfile, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon, IProcessProperty, ProcessPropertyType, ProcessCapability, IProcessPropertyMap, TitleEventSource } from 'vs/platform/terminal/common/terminal'; import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; @@ -100,24 +100,9 @@ export class RemoteTerminalChannelClient { get onProcessReplay(): Event<{ id: number, event: IPtyHostProcessReplayEvent }> { return this._channel.listen<{ id: number, event: IPtyHostProcessReplayEvent }>('$onProcessReplayEvent'); } - get onProcessTitleChanged(): Event<{ id: number, event: string }> { - return this._channel.listen<{ id: number, event: string }>('$onProcessTitleChangedEvent'); - } - get onProcessShellTypeChanged(): Event<{ id: number, event: TerminalShellType | undefined }> { - return this._channel.listen<{ id: number, event: TerminalShellType | undefined }>('$onProcessShellTypeChangedEvent'); - } - get onProcessOverrideDimensions(): Event<{ id: number, event: ITerminalDimensionsOverride | undefined }> { - return this._channel.listen<{ id: number, event: ITerminalDimensionsOverride | undefined }>('$onProcessOverrideDimensionsEvent'); - } - get onProcessResolvedShellLaunchConfig(): Event<{ id: number, event: IShellLaunchConfig }> { - return this._channel.listen<{ id: number, event: IShellLaunchConfig }>('$onProcessResolvedShellLaunchConfigEvent'); - } get onProcessOrphanQuestion(): Event<{ id: number }> { return this._channel.listen<{ id: number }>('$onProcessOrphanQuestion'); } - get onProcessDidChangeHasChildProcesses(): Event<{ id: number, event: boolean }> { - return this._channel.listen<{ id: number, event: boolean }>('$onProcessDidChangeHasChildProcesses'); - } get onExecuteCommand(): Event<{ reqId: number, commandId: string, commandArgs: any[] }> { return this._channel.listen<{ reqId: number, commandId: string, commandArgs: any[] }>('$onExecuteCommand'); } @@ -285,8 +270,8 @@ export class RemoteTerminalChannelClient { return this._channel.call<void>('$setTerminalLayoutInfo', args); } - updateTitle(id: number, title: string): Promise<string> { - return this._channel.call('$updateTitle', [id, title]); + updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise<string> { + return this._channel.call('$updateTitle', [id, title, titleSource]); } updateIcon(id: number, icon: TerminalIcon, color?: string): Promise<string> { diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index e427db091a3..7bf68c1f8ac 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -8,7 +8,7 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; import { IExtensionPointDescriptor } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon, TerminalLocationString, IProcessProperty, TerminalShellType, TitleEventSource, ProcessPropertyType, IFixedTerminalDimensions } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon, TerminalLocationString, IProcessProperty, TitleEventSource, ProcessPropertyType, IFixedTerminalDimensions, IExtensionTerminalProfile, ICreateContributedTerminalProfileOptions } from 'vs/platform/terminal/common/terminal'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; @@ -56,6 +56,25 @@ export interface ITerminalProfileResolverService { createProfileFromShellAndShellArgs(shell?: unknown, shellArgs?: unknown): Promise<ITerminalProfile | string>; } +export const ITerminalProfileService = createDecorator<ITerminalProfileService>('terminalProfileService'); +export interface ITerminalProfileService { + readonly _serviceBrand: undefined; + readonly availableProfiles: ITerminalProfile[]; + readonly contributedProfiles: IExtensionTerminalProfile[]; + readonly profilesReady: Promise<void>; + refreshAvailableProfiles(): void; + getDefaultProfileName(): string; + onDidChangeAvailableProfiles: Event<ITerminalProfile[]>; + getContributedDefaultProfile(shellLaunchConfig: IShellLaunchConfig): Promise<IExtensionTerminalProfile | undefined>; + registerContributedProfile(extensionIdentifier: string, id: string, title: string, options: ICreateContributedTerminalProfileOptions): Promise<void>; + getContributedProfileProvider(extensionIdentifier: string, id: string): ITerminalProfileProvider | undefined; + registerTerminalProfileProvider(extensionIdentifier: string, id: string, profileProvider: ITerminalProfileProvider): IDisposable; +} + +export interface ITerminalProfileProvider { + createContributedTerminalProfile(options: ICreateContributedTerminalProfileOptions): Promise<void>; +} + export interface IShellLaunchConfigResolveOptions { remoteAuthority: string | undefined; os: OperatingSystem; @@ -293,14 +312,9 @@ export interface ITerminalProcessManager extends IDisposable { readonly onProcessReady: Event<IProcessReadyEvent>; readonly onBeforeProcessData: Event<IBeforeProcessDataEvent>; readonly onProcessData: Event<IProcessDataEvent>; - readonly onProcessTitle: Event<string>; - readonly onProcessShellTypeChanged: Event<TerminalShellType>; - readonly onProcessExit: Event<number | undefined>; - readonly onProcessOverrideDimensions: Event<ITerminalDimensionsOverride | undefined>; - readonly onProcessResolvedShellLaunchConfig: Event<IShellLaunchConfig>; - readonly onProcessDidChangeHasChildProcesses: Event<boolean>; readonly onEnvironmentVariableInfoChanged: Event<IEnvironmentVariableInfo>; readonly onDidChangeProperty: Event<IProcessProperty<any>>; + readonly onProcessExit: Event<number | undefined>; dispose(immediate?: boolean): void; detachFromProcess(): Promise<void>; @@ -344,14 +358,10 @@ export interface ITerminalProcessExtHostProxy extends IDisposable { readonly instanceId: number; emitData(data: string): void; - emitTitle(title: string): void; + emitProcessProperty(property: IProcessProperty<any>): void; emitReady(pid: number, cwd: string): void; - emitExit(exitCode: number | undefined): void; - emitOverrideDimensions(dimensions: ITerminalDimensions | undefined): void; - emitResolvedShellLaunchConfig(shellLaunchConfig: IShellLaunchConfig): void; - emitInitialCwd(initialCwd: string): void; - emitCwd(cwd: string): void; emitLatency(latency: number): void; + emitExit(exitCode: number | undefined): void; onInput: Event<string>; onBinary: Event<string>; diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts index d1db62fc5fb..4e8837ee771 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts @@ -6,8 +6,9 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { ILocalPtyService } from 'vs/platform/terminal/electron-sandbox/terminal'; -import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalLaunchError, IProcessProperty, IProcessPropertyMap, ProcessPropertyType, TerminalShellType, ProcessCapability } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, ITerminalChildProcess, ITerminalLaunchError, IProcessProperty, IProcessPropertyMap, ProcessPropertyType, ProcessCapability, IProcessReadyEvent } from 'vs/platform/terminal/common/terminal'; import { IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/terminalProcess'; +import { URI } from 'vs/base/common/uri'; /** * Responsible for establishing and maintaining a connection with an existing terminal process @@ -18,7 +19,12 @@ export class LocalPty extends Disposable implements ITerminalChildProcess { private _properties: IProcessPropertyMap = { cwd: '', initialCwd: '', - fixedDimensions: { cols: undefined, rows: undefined } + fixedDimensions: { cols: undefined, rows: undefined }, + title: '', + shellType: undefined, + hasChildProcesses: true, + resolvedShellLaunchConfig: {}, + overrideDimensions: undefined }; private _capabilities: ProcessCapability[] = []; get capabilities(): ProcessCapability[] { return this._capabilities; } @@ -26,22 +32,12 @@ export class LocalPty extends Disposable implements ITerminalChildProcess { readonly onProcessData = this._onProcessData.event; private readonly _onProcessReplay = this._register(new Emitter<IPtyHostProcessReplayEvent>()); readonly onProcessReplay = this._onProcessReplay.event; - private readonly _onProcessExit = this._register(new Emitter<number | undefined>()); - readonly onProcessExit = this._onProcessExit.event; private readonly _onProcessReady = this._register(new Emitter<IProcessReadyEvent>()); readonly onProcessReady = this._onProcessReady.event; - private readonly _onProcessTitleChanged = this._register(new Emitter<string>()); - readonly onProcessTitleChanged = this._onProcessTitleChanged.event; - private readonly _onProcessOverrideDimensions = this._register(new Emitter<ITerminalDimensionsOverride | undefined>()); - readonly onProcessOverrideDimensions = this._onProcessOverrideDimensions.event; - private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter<IShellLaunchConfig>()); - readonly onProcessResolvedShellLaunchConfig = this._onProcessResolvedShellLaunchConfig.event; - private readonly _onProcessShellTypeChanged = this._register(new Emitter<TerminalShellType>()); - readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; - private readonly _onDidChangeHasChildProcesses = this._register(new Emitter<boolean>()); - readonly onDidChangeHasChildProcesses = this._onDidChangeHasChildProcesses.event; private readonly _onDidChangeProperty = this._register(new Emitter<IProcessProperty<any>>()); readonly onDidChangeProperty = this._onDidChangeProperty.event; + private readonly _onProcessExit = this._register(new Emitter<number | undefined>()); + readonly onProcessExit = this._onProcessExit.event; constructor( readonly id: number, @@ -114,28 +110,20 @@ export class LocalPty extends Disposable implements ITerminalChildProcess { this._capabilities = e.capabilities; this._onProcessReady.fire(e); } - handleTitleChanged(e: string) { - this._onProcessTitleChanged.fire(e); - } - handleShellTypeChanged(e: TerminalShellType) { - this._onProcessShellTypeChanged.fire(e); - } - handleOverrideDimensions(e: ITerminalDimensionsOverride | undefined) { - this._onProcessOverrideDimensions.fire(e); - } - handleResolvedShellLaunchConfig(e: IShellLaunchConfig) { - this._onProcessResolvedShellLaunchConfig.fire(e); - } - handleDidChangeHasChildProcesses(e: boolean) { - this._onDidChangeHasChildProcesses.fire(e); - } - handleDidChangeProperty(e: IProcessProperty<any>) { - if (e.type === ProcessPropertyType.Cwd) { - this._properties.cwd = e.value; - } else if (e.type === ProcessPropertyType.InitialCwd) { - this._properties.initialCwd = e.value; + handleDidChangeProperty({ type, value }: IProcessProperty<any>) { + switch (type) { + case ProcessPropertyType.Cwd: + this._properties.cwd = value; + break; + case ProcessPropertyType.InitialCwd: + this._properties.initialCwd = value; + break; + case ProcessPropertyType.ResolvedShellLaunchConfig: + if (value.cwd && typeof value.cwd !== 'string') { + value.cwd = URI.revive(value.cwd); + } } - this._onDidChangeProperty.fire(e); + this._onDidChangeProperty.fire({ type, value }); } async handleReplay(e: IPtyHostProcessReplayEvent) { @@ -144,7 +132,7 @@ export class LocalPty extends Disposable implements ITerminalChildProcess { for (const innerEvent of e.events) { if (innerEvent.cols !== 0 || innerEvent.rows !== 0) { // never override with 0x0 as that is a marker for an unknown initial size - this._onProcessOverrideDimensions.fire({ cols: innerEvent.cols, rows: innerEvent.rows, forceExactSize: true }); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.OverrideDimensions, value: { cols: innerEvent.cols, rows: innerEvent.rows, forceExactSize: true } }); } const e: IProcessDataEvent = { data: innerEvent.data, trackCommit: true }; this._onProcessData.fire(e); @@ -155,7 +143,7 @@ export class LocalPty extends Disposable implements ITerminalChildProcess { } // remove size override - this._onProcessOverrideDimensions.fire(undefined); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.OverrideDimensions, value: undefined }); } handleOrphanQuestion() { diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts index e7bfeb298a9..81208eb418c 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts @@ -57,6 +57,7 @@ export class LocalTerminalService extends Disposable implements ILocalTerminalSe // Attach process listeners this._localPtyService.onProcessData(e => this._ptys.get(e.id)?.handleData(e.event)); + this._localPtyService.onDidChangeProperty(e => this._ptys.get(e.id)?.handleDidChangeProperty(e.property)); this._localPtyService.onProcessExit(e => { const pty = this._ptys.get(e.id); if (pty) { @@ -65,11 +66,6 @@ export class LocalTerminalService extends Disposable implements ILocalTerminalSe } }); this._localPtyService.onProcessReady(e => this._ptys.get(e.id)?.handleReady(e.event)); - this._localPtyService.onProcessTitleChanged(e => this._ptys.get(e.id)?.handleTitleChanged(e.event)); - this._localPtyService.onProcessOverrideDimensions(e => this._ptys.get(e.id)?.handleOverrideDimensions(e.event)); - this._localPtyService.onProcessResolvedShellLaunchConfig(e => this._ptys.get(e.id)?.handleResolvedShellLaunchConfig(e.event)); - this._localPtyService.onProcessDidChangeHasChildProcesses(e => this._ptys.get(e.id)?.handleDidChangeHasChildProcesses(e.event)); - this._localPtyService.onDidChangeProperty(e => this._ptys.get(e.id)?.handleDidChangeProperty(e.property)); this._localPtyService.onProcessReplay(e => this._ptys.get(e.id)?.handleReplay(e.event)); this._localPtyService.onProcessOrphanQuestion(e => this._ptys.get(e.id)?.handleOrphanQuestion()); this._localPtyService.onDidRequestDetach(e => this._onDidRequestDetach.fire(e)); diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/terminalNativeContribution.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/terminalNativeContribution.ts index 4b56bd3d8ed..a6fb2f96f1c 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/terminalNativeContribution.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/terminalNativeContribution.ts @@ -41,7 +41,7 @@ export class TerminalNativeContribution extends Disposable implements IWorkbench } private _onOsResume(): void { - this._terminalService.instances.forEach(instance => instance.forceRedraw()); + this._terminalService.instances.forEach(instance => instance.xterm?.forceRedraw()); } private async _onOpenFileRequest(request: INativeOpenFileRequest): Promise<void> { diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/terminalProfileResolverService.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/terminalProfileResolverService.ts index efd4d3a99f9..5b06d449e3d 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/terminalProfileResolverService.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/terminalProfileResolverService.ts @@ -5,10 +5,12 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IRemoteTerminalService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IRemoteTerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { BaseTerminalProfileResolverService } from 'vs/workbench/contrib/terminal/browser/terminalProfileResolverService'; -import { ILocalTerminalService } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ILocalTerminalService, ITerminalProfileService } from 'vs/workbench/contrib/terminal/common/terminal'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; @@ -20,11 +22,13 @@ export class ElectronTerminalProfileResolverService extends BaseTerminalProfileR @IConfigurationService configurationService: IConfigurationService, @IHistoryService historyService: IHistoryService, @ILogService logService: ILogService, - @ITerminalService terminalService: ITerminalService, + @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @ILocalTerminalService localTerminalService: ILocalTerminalService, @IRemoteTerminalService remoteTerminalService: IRemoteTerminalService, - @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, - @IRemoteAgentService remoteAgentService: IRemoteAgentService + @ITerminalProfileService terminalProfileService: ITerminalProfileService, + @IRemoteAgentService remoteAgentService: IRemoteAgentService, + @IStorageService storageService: IStorageService, + @INotificationService notificationService: INotificationService ) { super( { @@ -44,9 +48,11 @@ export class ElectronTerminalProfileResolverService extends BaseTerminalProfileR configurationResolverService, historyService, logService, - terminalService, + terminalProfileService, workspaceContextService, - remoteAgentService + remoteAgentService, + storageService, + notificationService ); } } diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalCommandTracker.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalCommandTracker.test.ts index 8a53a67dad1..22f17831da1 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalCommandTracker.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalCommandTracker.test.ts @@ -5,12 +5,12 @@ import * as assert from 'assert'; import { Terminal } from 'xterm'; -import { CommandTrackerAddon } from 'vs/workbench/contrib/terminal/browser/addons/commandTrackerAddon'; +import { CommandTrackerAddon } from 'vs/workbench/contrib/terminal/browser/xterm/commandTrackerAddon'; import { isWindows } from 'vs/base/common/platform'; -import { XTermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; +import { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; interface TestTerminal extends Terminal { - _core: XTermCore; + _core: IXtermCore; } const ROWS = 10; diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalProfileService.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalProfileService.test.ts new file mode 100644 index 00000000000..5d8f07bc2de --- /dev/null +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalProfileService.test.ts @@ -0,0 +1,302 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILocalTerminalService, IOffProcessTerminalService, ITerminalConfiguration } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { TestExtensionService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TerminalProfileService } from 'vs/workbench/contrib/terminal/browser/terminalProfileService'; +import { ITerminalContributionService } from 'vs/workbench/contrib/terminal/common/terminalExtensionPoints'; +import { IExtensionTerminalProfile, ITerminalProfile } from 'vs/platform/terminal/common/terminal'; +import { IRemoteTerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { isLinux, isWindows, OperatingSystem } from 'vs/base/common/platform'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { Codicon } from 'vs/base/common/codicons'; +import { deepStrictEqual } from 'assert'; +import { assert } from 'console'; +import { Emitter } from 'vs/base/common/event'; +class TestTerminalProfileService extends TerminalProfileService { + hasRefreshedProfiles: Promise<void> | undefined; + override refreshAvailableProfiles(): void { + this.hasRefreshedProfiles = this._refreshAvailableProfilesNow(); + } + refreshAndAwaitAvailableProfiles(): Promise<void> { + this.refreshAvailableProfiles(); + if (!this.hasRefreshedProfiles) { + throw new Error('has not refreshed profiles yet'); + } + return this.hasRefreshedProfiles; + } +} + +class TestTerminalExtensionService extends TestExtensionService { + readonly _onDidChangeExtensions = new Emitter<void>(); +} + +class TestTerminalContributionService implements ITerminalContributionService { + _serviceBrand: undefined; + terminalProfiles: readonly IExtensionTerminalProfile[] = []; + setProfiles(profiles: IExtensionTerminalProfile[]): void { + this.terminalProfiles = profiles; + } +} + +class TestOffProcessTerminalService implements Partial<IOffProcessTerminalService> { + private _profiles: ITerminalProfile[] = []; + private _hasReturnedNone = true; + async getProfiles(profiles: unknown, defaultProfile: unknown, includeDetectedProfiles?: boolean): Promise<ITerminalProfile[]> { + if (this._hasReturnedNone) { + return this._profiles; + } else { + this._hasReturnedNone = true; + return []; + } + } + setProfiles(profiles: ITerminalProfile[]) { + this._profiles = profiles; + } + setReturnNone() { + this._hasReturnedNone = false; + } +} + +class TestRemoteAgentService implements Partial<IRemoteAgentService> { + private _os: OperatingSystem | undefined; + setEnvironment(os: OperatingSystem) { + this._os = os; + } + async getEnvironment(): Promise<IRemoteAgentEnvironment | null> { + return { os: this._os } as IRemoteAgentEnvironment; + } +} + +const defaultTerminalConfig: Partial<ITerminalConfiguration> = { profiles: { windows: {}, linux: {}, osx: {} } }; +let powershellProfile = { + profileName: 'PowerShell', + path: 'C:\\Powershell.exe', + isDefault: true, + icon: ThemeIcon.asThemeIcon(Codicon.terminalPowershell) +}; +let jsdebugProfile = { + extensionIdentifier: 'ms-vscode.js-debug-nightly', + icon: 'debug', + id: 'extension.js-debug.debugTerminal', + title: 'JavaScript Debug Terminal' +}; + + +suite('TerminalProfileService', () => { + let configurationService: TestConfigurationService; + let terminalProfileService: TestTerminalProfileService; + let remoteAgentService: TestRemoteAgentService; + let localTerminalService: TestOffProcessTerminalService; + let remoteTerminalService: TestOffProcessTerminalService; + let extensionService: TestTerminalExtensionService; + let environmentService: IWorkbenchEnvironmentService; + let instantiationService: TestInstantiationService; + + setup(async () => { + configurationService = new TestConfigurationService({ terminal: { integrated: defaultTerminalConfig } }); + remoteAgentService = new TestRemoteAgentService(); + localTerminalService = new TestOffProcessTerminalService(); + remoteTerminalService = new TestOffProcessTerminalService(); + extensionService = new TestTerminalExtensionService(); + environmentService = { configuration: {}, remoteAuthority: undefined } as IWorkbenchEnvironmentService; + instantiationService = new TestInstantiationService(); + + let terminalContributionService = new TestTerminalContributionService(); + let contextKeyService = new MockContextKeyService(); + + instantiationService.stub(IContextKeyService, contextKeyService); + instantiationService.stub(IExtensionService, extensionService); + instantiationService.stub(IConfigurationService, configurationService); + instantiationService.stub(IRemoteAgentService, remoteAgentService); + instantiationService.stub(ITerminalContributionService, terminalContributionService); + instantiationService.stub(ILocalTerminalService, localTerminalService); + instantiationService.stub(IRemoteTerminalService, remoteTerminalService); + instantiationService.stub(IWorkbenchEnvironmentService, environmentService); + + terminalProfileService = instantiationService.createInstance(TestTerminalProfileService); + + //reset as these properties are changed in each test + powershellProfile = { + profileName: 'PowerShell', + path: 'C:\\Powershell.exe', + isDefault: true, + icon: ThemeIcon.asThemeIcon(Codicon.terminalPowershell) + }; + jsdebugProfile = { + extensionIdentifier: 'ms-vscode.js-debug-nightly', + icon: 'debug', + id: 'extension.js-debug.debugTerminal', + title: 'JavaScript Debug Terminal' + }; + + localTerminalService.setProfiles([powershellProfile]); + remoteTerminalService.setProfiles([]); + terminalContributionService.setProfiles([jsdebugProfile]); + if (isWindows) { + remoteAgentService.setEnvironment(OperatingSystem.Windows); + } else if (isLinux) { + remoteAgentService.setEnvironment(OperatingSystem.Linux); + } else { + remoteAgentService.setEnvironment(OperatingSystem.Macintosh); + } + configurationService.setUserConfiguration('terminal', { integrated: defaultTerminalConfig }); + }); + suite('Contributed Profiles', () => { + test('should filter out contributed profiles set to null', async () => { + await configurationService.setUserConfiguration('terminal', { + integrated: { + profiles: { + windows: { + 'JavaScript Debug Terminal': null + }, + linux: { + 'JavaScript Debug Terminal': null + }, + osx: { + 'JavaScript Debug Terminal': null + } + } + } + }); + configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true } as any); + await terminalProfileService.refreshAndAwaitAvailableProfiles(); + deepStrictEqual(terminalProfileService.availableProfiles, [powershellProfile]); + deepStrictEqual(terminalProfileService.contributedProfiles, []); + }); + test('should include contributed profiles', async () => { + await terminalProfileService.refreshAndAwaitAvailableProfiles(); + deepStrictEqual(terminalProfileService.availableProfiles, [powershellProfile]); + deepStrictEqual(terminalProfileService.contributedProfiles, [jsdebugProfile]); + }); + }); + + test('should get profiles from remoteTerminalService when there is a remote authority', async () => { + environmentService = { configuration: {}, remoteAuthority: 'authority' } as IWorkbenchEnvironmentService; + instantiationService.stub(IWorkbenchEnvironmentService, environmentService); + terminalProfileService = instantiationService.createInstance(TestTerminalProfileService); + await terminalProfileService.refreshAndAwaitAvailableProfiles(); + deepStrictEqual(terminalProfileService.availableProfiles, []); + deepStrictEqual(terminalProfileService.contributedProfiles, [jsdebugProfile]); + remoteTerminalService.setProfiles([powershellProfile]); + await terminalProfileService.refreshAndAwaitAvailableProfiles(); + deepStrictEqual(terminalProfileService.availableProfiles, [powershellProfile]); + deepStrictEqual(terminalProfileService.contributedProfiles, [jsdebugProfile]); + }); + + test('should fire onDidChangeAvailableProfiles only when available profiles have changed via user config', async () => { + powershellProfile.icon = ThemeIcon.asThemeIcon(Codicon.lightBulb); + let calls: ITerminalProfile[] = []; + let countCalled = 0; + await new Promise<void>(r => { + terminalProfileService.onDidChangeAvailableProfiles(e => { + calls.push(...e); + countCalled++; + r(); + }); + }); + await configurationService.setUserConfiguration('terminal', { + integrated: { + profiles: { + windows: powershellProfile, + linux: powershellProfile, + osx: powershellProfile + } + } + }); + configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true } as any); + await terminalProfileService.refreshAndAwaitAvailableProfiles(); + assert(countCalled === 1, true); + deepStrictEqual(calls, [powershellProfile]); + deepStrictEqual(terminalProfileService.availableProfiles, [powershellProfile]); + deepStrictEqual(terminalProfileService.contributedProfiles, [jsdebugProfile]); + calls = []; + countCalled = 0; + await terminalProfileService.refreshAndAwaitAvailableProfiles(); + assert(countCalled === 0, true); + deepStrictEqual(calls, []); + }); + + test('should fire onDidChangeAvailableProfiles when available or contributed profiles have changed via remote/localTerminalService', async () => { + powershellProfile.isDefault = false; + localTerminalService.setProfiles([powershellProfile]); + const calls: ITerminalProfile[] = []; + let countCalled = 0; + await new Promise<void>(r => { + terminalProfileService.onDidChangeAvailableProfiles(e => { + calls.push(...e); + countCalled++; + r(); + }); + }); + await terminalProfileService.refreshAndAwaitAvailableProfiles(); + assert(countCalled === 1, true); + deepStrictEqual(calls, [powershellProfile]); + deepStrictEqual(terminalProfileService.availableProfiles, [powershellProfile]); + deepStrictEqual(terminalProfileService.contributedProfiles, [jsdebugProfile]); + }); + + test('should fire onDidChangeAvailableProfiles when available or contributed profiles have changed via remote/localTerminalService', async () => { + powershellProfile.isDefault = false; + localTerminalService.setProfiles([powershellProfile]); + const calls: ITerminalProfile[] = []; + let countCalled = 0; + await new Promise<void>(r => { + terminalProfileService.onDidChangeAvailableProfiles(e => { + calls.push(...e); + countCalled++; + r(); + }); + }); + await terminalProfileService.refreshAndAwaitAvailableProfiles(); + assert(countCalled === 1, true); + deepStrictEqual(calls, [powershellProfile]); + deepStrictEqual(terminalProfileService.availableProfiles, [powershellProfile]); + deepStrictEqual(terminalProfileService.contributedProfiles, [jsdebugProfile]); + }); + + test('should call refreshAvailableProfiles _onDidChangeExtensions', async () => { + extensionService._onDidChangeExtensions.fire(); + const calls: ITerminalProfile[] = []; + let countCalled = 0; + await new Promise<void>(r => { + terminalProfileService.onDidChangeAvailableProfiles(e => { + calls.push(...e); + countCalled++; + r(); + }); + }); + assert(countCalled === 1, true); + deepStrictEqual(calls, [powershellProfile]); + deepStrictEqual(terminalProfileService.availableProfiles, [powershellProfile]); + deepStrictEqual(terminalProfileService.contributedProfiles, [jsdebugProfile]); + }); + test('should call refreshAvailableProfiles again if no profiles are returned from local/remoteTerminalService', async () => { + localTerminalService.setReturnNone(); + const calls: ITerminalProfile[] = []; + let countCalled = 0; + await new Promise<void>(r => { + terminalProfileService.onDidChangeAvailableProfiles(e => { + calls.push(...e); + countCalled++; + r(); + }); + }); + await terminalProfileService.refreshAndAwaitAvailableProfiles(); + assert(countCalled === 1, true); + deepStrictEqual(calls, [powershellProfile]); + deepStrictEqual(terminalProfileService.availableProfiles, [powershellProfile]); + deepStrictEqual(terminalProfileService.contributedProfiles, [jsdebugProfile]); + }); +}); diff --git a/src/vs/workbench/contrib/terminal/test/browser/addons/lineDataEventAddon.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/lineDataEventAddon.test.ts similarity index 98% rename from src/vs/workbench/contrib/terminal/test/browser/addons/lineDataEventAddon.test.ts rename to src/vs/workbench/contrib/terminal/test/browser/xterm/lineDataEventAddon.test.ts index 2ace440dd9e..adda5c0755d 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/addons/lineDataEventAddon.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/lineDataEventAddon.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Terminal } from 'xterm'; -import { LineDataEventAddon } from 'vs/workbench/contrib/terminal/browser/addons/lineDataEventAddon'; +import { LineDataEventAddon } from 'vs/workbench/contrib/terminal/browser/xterm/lineDataEventAddon'; import { OperatingSystem } from 'vs/base/common/platform'; import { deepStrictEqual } from 'assert'; diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts new file mode 100644 index 00000000000..100f7654d0c --- /dev/null +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts @@ -0,0 +1,271 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IEvent, Terminal } from 'xterm'; +import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; +import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ITerminalConfigHelper, ITerminalConfiguration, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; +import { deepStrictEqual, strictEqual } from 'assert'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { TestColorTheme, TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IViewDescriptor, IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { Emitter } from 'vs/base/common/event'; +import { TERMINAL_BACKGROUND_COLOR, TERMINAL_FOREGROUND_COLOR, TERMINAL_CURSOR_FOREGROUND_COLOR, TERMINAL_CURSOR_BACKGROUND_COLOR, TERMINAL_SELECTION_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; +import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { WebglAddon } from 'xterm-addon-webgl'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; +import { isSafari } from 'vs/base/browser/browser'; + +class TestWebglAddon { + static shouldThrow = false; + static isEnabled = false; + readonly onContextLoss = new Emitter().event as IEvent<void>; + activate() { + TestWebglAddon.isEnabled = !TestWebglAddon.shouldThrow; + if (TestWebglAddon.shouldThrow) { + throw new Error('Test webgl set to throw'); + } + } + dispose() { + TestWebglAddon.isEnabled = false; + } + clearTextureAtlas() { } +} + +class TestXtermTerminal extends XtermTerminal { + webglAddonPromise: Promise<typeof WebglAddon> = Promise.resolve(TestWebglAddon); + protected override _getWebglAddonConstructor() { + // Force synchronous to avoid async when activating the addon + return this.webglAddonPromise; + } +} + +class TestViewDescriptorService implements Partial<IViewDescriptorService> { + private _location = ViewContainerLocation.Panel; + private _onDidChangeLocation = new Emitter<{ views: IViewDescriptor[], from: ViewContainerLocation, to: ViewContainerLocation }>(); + onDidChangeLocation = this._onDidChangeLocation.event; + getViewLocationById(id: string) { + return this._location; + } + moveTerminalToLocation(to: ViewContainerLocation) { + const oldLocation = this._location; + this._location = to; + this._onDidChangeLocation.fire({ + views: [ + { id: TERMINAL_VIEW_ID } as any + ], + from: oldLocation, + to + }); + } +} + +const defaultTerminalConfig: Partial<ITerminalConfiguration> = { + fontFamily: 'monospace', + fontWeight: 'normal', + fontWeightBold: 'normal', + gpuAcceleration: 'off', + scrollback: 1000, + fastScrollSensitivity: 2, + mouseWheelScrollSensitivity: 1, + unicodeVersion: '11' +}; + +suite('XtermTerminal', () => { + let instantiationService: TestInstantiationService; + let configurationService: TestConfigurationService; + let themeService: TestThemeService; + let viewDescriptorService: TestViewDescriptorService; + + let xterm: TestXtermTerminal; + let configHelper: ITerminalConfigHelper; + + setup(() => { + configurationService = new TestConfigurationService({ + editor: { + fastScrollSensitivity: 2, + mouseWheelScrollSensitivity: 1 + } as Partial<IEditorOptions>, + terminal: { + integrated: defaultTerminalConfig + } + }); + themeService = new TestThemeService(); + viewDescriptorService = new TestViewDescriptorService(); + + instantiationService = new TestInstantiationService(); + instantiationService.stub(IConfigurationService, configurationService); + instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(IStorageService, new TestStorageService()); + instantiationService.stub(IThemeService, themeService); + instantiationService.stub(IViewDescriptorService, viewDescriptorService); + + configHelper = instantiationService.createInstance(TerminalConfigHelper); + xterm = instantiationService.createInstance(TestXtermTerminal, Terminal, configHelper, 80, 30); + + TestWebglAddon.shouldThrow = false; + TestWebglAddon.isEnabled = false; + }); + + test('should use fallback dimensions of 80x30', () => { + strictEqual(xterm.raw.getOption('cols'), 80); + strictEqual(xterm.raw.getOption('rows'), 30); + }); + + suite('theme', () => { + test('should apply correct background color based on the current view', () => { + themeService.setTheme(new TestColorTheme({ + [PANEL_BACKGROUND]: '#ff0000', + [SIDE_BAR_BACKGROUND]: '#00ff00' + })); + xterm = instantiationService.createInstance(XtermTerminal, Terminal, configHelper, 80, 30); + strictEqual(xterm.raw.getOption('theme').background, '#ff0000'); + viewDescriptorService.moveTerminalToLocation(ViewContainerLocation.Sidebar); + strictEqual(xterm.raw.getOption('theme').background, '#00ff00'); + viewDescriptorService.moveTerminalToLocation(ViewContainerLocation.Panel); + strictEqual(xterm.raw.getOption('theme').background, '#ff0000'); + viewDescriptorService.moveTerminalToLocation(ViewContainerLocation.AuxiliaryBar); + strictEqual(xterm.raw.getOption('theme').background, '#00ff00'); + }); + test('should react to and apply theme changes', () => { + themeService.setTheme(new TestColorTheme({ + [TERMINAL_BACKGROUND_COLOR]: '#000100', + [TERMINAL_FOREGROUND_COLOR]: '#000200', + [TERMINAL_CURSOR_FOREGROUND_COLOR]: '#000300', + [TERMINAL_CURSOR_BACKGROUND_COLOR]: '#000400', + [TERMINAL_SELECTION_BACKGROUND_COLOR]: '#000500', + 'terminal.ansiBlack': '#010000', + 'terminal.ansiRed': '#020000', + 'terminal.ansiGreen': '#030000', + 'terminal.ansiYellow': '#040000', + 'terminal.ansiBlue': '#050000', + 'terminal.ansiMagenta': '#060000', + 'terminal.ansiCyan': '#070000', + 'terminal.ansiWhite': '#080000', + 'terminal.ansiBrightBlack': '#090000', + 'terminal.ansiBrightRed': '#100000', + 'terminal.ansiBrightGreen': '#110000', + 'terminal.ansiBrightYellow': '#120000', + 'terminal.ansiBrightBlue': '#130000', + 'terminal.ansiBrightMagenta': '#140000', + 'terminal.ansiBrightCyan': '#150000', + 'terminal.ansiBrightWhite': '#160000', + })); + xterm = instantiationService.createInstance(XtermTerminal, Terminal, configHelper, 80, 30); + deepStrictEqual(xterm.raw.getOption('theme'), { + background: '#000100', + foreground: '#000200', + cursor: '#000300', + cursorAccent: '#000400', + selection: '#000500', + black: '#010000', + green: '#030000', + red: '#020000', + yellow: '#040000', + blue: '#050000', + magenta: '#060000', + cyan: '#070000', + white: '#080000', + brightBlack: '#090000', + brightRed: '#100000', + brightGreen: '#110000', + brightYellow: '#120000', + brightBlue: '#130000', + brightMagenta: '#140000', + brightCyan: '#150000', + brightWhite: '#160000', + }); + themeService.setTheme(new TestColorTheme({ + [TERMINAL_BACKGROUND_COLOR]: '#00010f', + [TERMINAL_FOREGROUND_COLOR]: '#00020f', + [TERMINAL_CURSOR_FOREGROUND_COLOR]: '#00030f', + [TERMINAL_CURSOR_BACKGROUND_COLOR]: '#00040f', + [TERMINAL_SELECTION_BACKGROUND_COLOR]: '#00050f', + 'terminal.ansiBlack': '#01000f', + 'terminal.ansiRed': '#02000f', + 'terminal.ansiGreen': '#03000f', + 'terminal.ansiYellow': '#04000f', + 'terminal.ansiBlue': '#05000f', + 'terminal.ansiMagenta': '#06000f', + 'terminal.ansiCyan': '#07000f', + 'terminal.ansiWhite': '#08000f', + 'terminal.ansiBrightBlack': '#09000f', + 'terminal.ansiBrightRed': '#10000f', + 'terminal.ansiBrightGreen': '#11000f', + 'terminal.ansiBrightYellow': '#12000f', + 'terminal.ansiBrightBlue': '#13000f', + 'terminal.ansiBrightMagenta': '#14000f', + 'terminal.ansiBrightCyan': '#15000f', + 'terminal.ansiBrightWhite': '#16000f', + })); + deepStrictEqual(xterm.raw.getOption('theme'), { + background: '#00010f', + foreground: '#00020f', + cursor: '#00030f', + cursorAccent: '#00040f', + selection: '#00050f', + black: '#01000f', + green: '#03000f', + red: '#02000f', + yellow: '#04000f', + blue: '#05000f', + magenta: '#06000f', + cyan: '#07000f', + white: '#08000f', + brightBlack: '#09000f', + brightRed: '#10000f', + brightGreen: '#11000f', + brightYellow: '#12000f', + brightBlue: '#13000f', + brightMagenta: '#14000f', + brightCyan: '#15000f', + brightWhite: '#16000f', + }); + }); + }); + + suite('renderers', () => { + test('should re-evaluate gpu acceleration auto when the setting is changed', async () => { + // Check initial state + strictEqual(xterm.raw.getOption('rendererType'), 'dom'); + strictEqual(TestWebglAddon.isEnabled, false); + + // Open xterm as otherwise the webgl addon won't activate + const container = document.createElement('div'); + xterm.raw.open(container); + + // Auto should activate the webgl addon + await configurationService.setUserConfiguration('terminal', { integrated: { ...defaultTerminalConfig, gpuAcceleration: 'auto' } }); + configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true } as any); + await xterm.webglAddonPromise; // await addon activate + if (isSafari) { + strictEqual(TestWebglAddon.isEnabled, false, 'The webgl renderer is always disabled on Safari'); + } else { + strictEqual(TestWebglAddon.isEnabled, true); + } + + // Turn off to reset state + await configurationService.setUserConfiguration('terminal', { integrated: { ...defaultTerminalConfig, gpuAcceleration: 'off' } }); + configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true } as any); + await xterm.webglAddonPromise; // await addon activate + strictEqual(xterm.raw.getOption('rendererType'), 'dom'); + strictEqual(TestWebglAddon.isEnabled, false); + + // Set to auto again but throw when activating the webgl addon + TestWebglAddon.shouldThrow = true; + await configurationService.setUserConfiguration('terminal', { integrated: { ...defaultTerminalConfig, gpuAcceleration: 'auto' } }); + configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true } as any); + await xterm.webglAddonPromise; // await addon activate + strictEqual(xterm.raw.getOption('rendererType'), 'canvas'); + strictEqual(TestWebglAddon.isEnabled, false); + }); + }); +}); diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputTerminalService.ts b/src/vs/workbench/contrib/testing/browser/testingOutputTerminalService.ts index 96f0f8e76a9..cfe41da20b7 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputTerminalService.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputTerminalService.ts @@ -10,7 +10,7 @@ import { listenStream } from 'vs/base/common/stream'; import { isDefined } from 'vs/base/common/types'; import { localize } from 'vs/nls'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IProcessDataEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalLaunchError, ProcessCapability, ProcessPropertyType, TerminalLocation, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IProcessPropertyMap, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalLaunchError, ProcessCapability, ProcessPropertyType, TerminalLocation, TerminalShellType } from 'vs/platform/terminal/common/terminal'; import { IViewsService } from 'vs/workbench/common/views'; import { ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -157,9 +157,6 @@ export class TestingOutputTerminalService implements ITestingOutputTerminalServi } class TestOutputProcess extends Disposable implements ITerminalChildProcess { - updateProperty(property: ProcessPropertyType, value: any): Promise<void> { - throw new Error('Method not implemented.'); - } onProcessOverrideDimensions?: Event<ITerminalDimensionsOverride | undefined> | undefined; onProcessResolvedShellLaunchConfig?: Event<IShellLaunchConfig> | undefined; onDidChangeHasChildProcesses?: Event<boolean> | undefined; @@ -238,8 +235,12 @@ class TestOutputProcess extends Disposable implements ITerminalChildProcess { return Promise.resolve(0); } - refreshProperty(property: ProcessPropertyType) { - return Promise.resolve(''); + public refreshProperty<T extends ProcessPropertyType>(property: ProcessPropertyType): Promise<IProcessPropertyMap[T]> { + throw new Error(`refreshProperty is not suppported in TestOutputProcesses. property: ${property}`); + } + + public updateProperty(property: ProcessPropertyType, value: any): Promise<void> { + throw new Error(`updateProperty is not suppported in TestOutputProcesses. property: ${property}, value: ${value}`); } //#endregion } diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.contribution.ts b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.contribution.ts index b79f4f85cb5..848cd9cd251 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.contribution.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.contribution.ts @@ -25,7 +25,7 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { EditorResolution } from 'vs/platform/editor/common/editor'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; +import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { isLinux, isMacintosh, isWindows, OperatingSystem as OS } from 'vs/base/common/platform'; @@ -230,12 +230,12 @@ class WorkbenchConfigurationContribution { constructor( @IInstantiationService _instantiationService: IInstantiationService, @IConfigurationService _configurationService: IConfigurationService, - @ITASExperimentService _experimentSevice: ITASExperimentService, + @IWorkbenchAssignmentService _experimentSevice: IWorkbenchAssignmentService, ) { this.registerConfigs(_experimentSevice); } - private async registerConfigs(_experimentSevice: ITASExperimentService) { + private async registerConfigs(_experimentSevice: IWorkbenchAssignmentService) { const preferReduced = await _experimentSevice.getTreatment('welcomePage.preferReducedMotion').catch(e => false); if (preferReduced) { configurationRegistry.updateConfigurations({ add: [prefersReducedMotionConfig], remove: [prefersStandardMotionConfig] }); diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.css b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.css index 19cc2b251eb..26d4d8a30db 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.css +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.css @@ -282,10 +282,12 @@ .monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-category .new-badge { justify-self: flex-end; + align-self: flex-start; border-radius: 4px; padding: 2px 4px; - margin: 0 4px; + margin: 4px; font-size: 11px; + white-space: nowrap; } .monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-category .featured-badge { @@ -472,11 +474,11 @@ .monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlideDetails .gettingStartedDetailsContent { height: 100%; - max-width: 1200px; + max-width: 1600px; margin: 0 auto; - padding: 0 32px; + padding: 0 0 0 32px; display: grid; - grid-template-columns: 1fr 5fr 1fr 7fr 1fr; + grid-template-columns: 1fr 4fr 1fr 8fr; grid-template-rows: calc(25% - 100px) auto auto 1fr auto; grid-template-areas: ". back . media ." @@ -736,6 +738,11 @@ margin: 0; } +.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlideCategories>.gettingStartedCategoriesContainer .index-list.start-container { + min-height: 156px; + margin-bottom: 16px; +} + .monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlideCategories>.gettingStartedCategoriesContainer>.footer>button { text-align: center; } diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts index 21361538329..3f708a5f9a7 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts @@ -35,7 +35,7 @@ import { IWindowOpenable } from 'vs/platform/windows/common/windows'; import { splitName } from 'vs/base/common/labels'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { isMacintosh, locale } from 'vs/base/common/platform'; -import { Throttler } from 'vs/base/common/async'; +import { Delayer, Throttler } from 'vs/base/common/async'; import { GettingStartedInput } from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedInput'; import { GroupDirection, GroupsOrder, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; @@ -522,14 +522,24 @@ export class GettingStartedPage extends EditorPane { StorageTarget.USER); } + private currentMediaComponent: string | undefined = undefined; private async buildMediaComponent(stepId: string) { if (!this.currentWalkthrough) { throw Error('no walkthrough selected'); } const stepToExpand = assertIsDefined(this.currentWalkthrough.steps.find(step => step.id === stepId)); + if (this.currentMediaComponent === stepId) { return; } + this.currentMediaComponent = stepId; + this.stepDisposables.clear(); - clearNode(this.stepMediaComponent); + + this.stepDisposables.add({ + dispose: () => { + clearNode(this.stepMediaComponent); + this.currentMediaComponent = undefined; + } + }); if (stepToExpand.media.type === 'image') { @@ -621,6 +631,15 @@ export class GettingStartedPage extends EditorPane { } }; + if (serializedContextKeyExprs) { + const contextKeyExprs = coalesce(serializedContextKeyExprs.map(expr => ContextKeyExpr.deserialize(expr))); + const watchingKeys = new Set(flatten(contextKeyExprs.map(expr => expr.keys()))); + + this.stepDisposables.add(this.contextService.onDidChangeContext(e => { + if (e.affectsSome(watchingKeys)) { postTrueKeysMessage(); } + })); + } + let isDisposed = false; this.stepDisposables.add(toDisposable(() => { isDisposed = true; })); @@ -639,32 +658,29 @@ export class GettingStartedPage extends EditorPane { } })); - if (serializedContextKeyExprs) { - const contextKeyExprs = coalesce(serializedContextKeyExprs.map(expr => ContextKeyExpr.deserialize(expr))); - const watchingKeys = new Set(flatten(contextKeyExprs.map(expr => expr.keys()))); + const layoutDelayer = new Delayer(50); - this.stepDisposables.add(this.contextService.onDidChangeContext(e => { - if (e.affectsSome(watchingKeys)) { postTrueKeysMessage(); } - })); - - this.layoutMarkdown = () => { webview.postMessage({ layout: true }); }; - this.stepDisposables.add({ dispose: () => this.layoutMarkdown = undefined }); - this.layoutMarkdown(); - - postTrueKeysMessage(); - - webview.onMessage(e => { - const message: string = e.message as string; - if (message.startsWith('command$')) { - this.openerService.open(message.replace('$', ':'), { allowCommands: true }); - } else if (message.startsWith('setTheme$')) { - this.configurationService.updateValue(ThemeSettings.COLOR_THEME, message.slice('setTheme:'.length), ConfigurationTarget.USER); - } else { - console.error('Unexpected message', message); - } + this.layoutMarkdown = () => { + layoutDelayer.trigger(() => { + webview.postMessage({ layoutMeNow: true }); }); - } + }; + this.stepDisposables.add(layoutDelayer); + this.stepDisposables.add({ dispose: () => this.layoutMarkdown = undefined }); + + postTrueKeysMessage(); + + this.stepDisposables.add(webview.onMessage(e => { + const message: string = e.message as string; + if (message.startsWith('command:')) { + this.openerService.open(message, { allowCommands: true }); + } else if (message.startsWith('setTheme:')) { + this.configurationService.updateValue(ThemeSettings.COLOR_THEME, message.slice('setTheme:'.length), ConfigurationTarget.USER); + } else { + console.error('Unexpected message', message); + } + })); } } @@ -808,13 +824,26 @@ export class GettingStartedPage extends EditorPane { margin-block-end: 0.25em; margin-block-start: 0.25em; } + vertically-centered { + padding-top: 5px; + padding-bottom: 5px; + } html { height: 100%; + padding-right: 32px; + } + h1 { + font-size: 19.5px; + } + h2 { + font-size: 18.5px; } </style> </head> <body> - ${uriTranformedContent} + <vertically-centered> + ${uriTranformedContent} + </vertically-centered> </body> <script nonce="${nonce}"> const vscode = acquireVsCodeApi(); @@ -824,10 +853,31 @@ export class GettingStartedPage extends EditorPane { }); }); - window.addEventListener('message', event => { + let ongoingLayout = undefined; + const doLayout = () => { document.querySelectorAll('vertically-centered').forEach(element => { - element.style.marginTop = Math.max((document.body.scrollHeight - element.scrollHeight) * 2/5, 10) + 'px'; - }) + element.style.marginTop = Math.max((document.body.clientHeight - element.scrollHeight) * 3/10, 0) + 'px'; + }); + ongoingLayout = undefined; + }; + + const layout = () => { + if (ongoingLayout) { + clearTimeout(ongoingLayout); + } + ongoingLayout = setTimeout(doLayout, 0); + }; + + layout(); + + document.querySelectorAll('img').forEach(element => { + element.onload = layout; + }) + + window.addEventListener('message', event => { + if (event.data.layoutMeNow) { + layout(); + } if (event.data.enabledContextKeys) { document.querySelectorAll('.checked').forEach(element => element.classList.remove('checked')) for (const key of event.data.enabledContextKeys) { @@ -1398,7 +1448,7 @@ export class GettingStartedPage extends EditorPane { 'data-step-id': step.id, 'aria-expanded': 'false', 'aria-checked': '' + step.done, - 'role': 'listitem', + 'role': 'button', }, codicon, stepDescription); diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService.ts b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService.ts index 57ce1f03111..d74192720f0 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService.ts @@ -19,7 +19,7 @@ import { FileAccess } from 'vs/base/common/network'; import { DefaultIconPath, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { walkthroughs } from 'vs/workbench/contrib/welcome/gettingStarted/common/gettingStartedContent'; -import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; +import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILink, LinkedText, parseLinkedText } from 'vs/base/common/linkedText'; @@ -135,7 +135,7 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ private gettingStartedContributions = new Map<string, IWalkthrough>(); private steps = new Map<string, IWalkthroughStep>(); - private tasExperimentService?: ITASExperimentService; + private tasExperimentService?: IWorkbenchAssignmentService; private sessionInstalledExtensions = new Set<string>(); private categoryVisibilityContextKeys = new Set<string>(); @@ -159,7 +159,7 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ @IHostService private readonly hostService: IHostService, @IViewsService private readonly viewsService: IViewsService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @ITASExperimentService tasExperimentService: ITASExperimentService, + @IWorkbenchAssignmentService tasExperimentService: IWorkbenchAssignmentService, ) { super(); diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/common/gettingStartedContent.ts b/src/vs/workbench/contrib/welcome/gettingStarted/common/gettingStartedContent.ts index 32c703a6657..62c5ac5b7fb 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/common/gettingStartedContent.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/common/gettingStartedContent.ts @@ -144,7 +144,7 @@ export const startEntries: GettingStartedStartEntryContent = [ { id: 'topLevelShowWalkthroughs', title: localize('gettingStarted.topLevelShowWalkthroughs.title', "Open a Walkthrough..."), - description: localize('gettingStarted.topLevelShowWalkthroughs.description', ""), + description: localize('gettingStarted.topLevelShowWalkthroughs.description', "View a walkthrough on the editor or an extension"), icon: Codicon.checklist, when: 'allWalkthroughsHidden', content: { diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/common/media/example_markdown_media.ts b/src/vs/workbench/contrib/welcome/gettingStarted/common/media/example_markdown_media.ts index e277c344fe0..ddc6e9b4a95 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/common/media/example_markdown_media.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/common/media/example_markdown_media.ts @@ -7,23 +7,21 @@ import { escape } from 'vs/base/common/strings'; import { localize } from 'vs/nls'; export default () => ` -<vertically-centered> <checklist> - <checkbox when-checked="setTheme$Default Light+" checked-on="config.workbench.colorTheme == 'Default Light+'"> + <checkbox when-checked="setTheme:Default Light+" checked-on="config.workbench.colorTheme == 'Default Light+'"> <img width="150" src="./light.png"/> ${escape(localize('light', "Light"))} </checkbox> - <checkbox when-checked="setTheme$Default Dark+" checked-on="config.workbench.colorTheme == 'Default Dark+'"> + <checkbox when-checked="setTheme:Default Dark+" checked-on="config.workbench.colorTheme == 'Default Dark+'"> <img width="150" src="./dark.png"/> ${escape(localize('dark', "Dark"))} </checkbox> - <checkbox when-checked="setTheme$Default High Contrast" checked-on="config.workbench.colorTheme == 'Default High Contrast'"> + <checkbox when-checked="setTheme:Default High Contrast" checked-on="config.workbench.colorTheme == 'Default High Contrast'"> <img width="150" src="./monokai.png"/> ${escape(localize('HighContrast', "High Contrast"))} </checkbox> </checklist> -<checkbox when-checked="command$workbench.action.selectTheme" checked-on="false"> +<checkbox when-checked="command:workbench.action.selectTheme" checked-on="false"> ${escape(localize('seeMore', "See More Themes..."))} </checkbox> -</vertically-centered> `; diff --git a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts index 7aac74af142..a930df5bbc0 100644 --- a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts +++ b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts @@ -58,7 +58,6 @@ export class WelcomePageContribution implements IWorkbenchContribution { && !this.environmentService.skipWelcome && !this.storageService.get(telemetryOptOutStorageKey, StorageScope.GLOBAL) ) { - this.storageService.store(telemetryOptOutStorageKey, true, StorageScope.GLOBAL, StorageTarget.USER); await this.openWelcome(true); return; diff --git a/src/vs/workbench/electron-browser/desktop.main.ts b/src/vs/workbench/electron-browser/desktop.main.ts index 6a7d67b1b3b..3f9fb0e041f 100644 --- a/src/vs/workbench/electron-browser/desktop.main.ts +++ b/src/vs/workbench/electron-browser/desktop.main.ts @@ -14,7 +14,6 @@ import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { SharedDesktopMain } from 'vs/workbench/electron-sandbox/shared.desktop.main'; import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { ISharedProcessWorkerWorkbenchService } from 'vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessWorkerWorkbenchService'; -import product from 'vs/platform/product/common/product'; class DesktopMain extends SharedDesktopMain { @@ -28,10 +27,10 @@ class DesktopMain extends SharedDesktopMain { // Local Files let diskFileSystemProvider: ElectronFileSystemProvider | SandboxedDiskFileSystemProvider; - if (this.configuration.experimentalSandboxedFileService ?? product.quality !== 'stable') { - logService.info('[FileService]: Using sandbox ready file system provider'); + if (this.configuration.experimentalSandboxedFileService !== false) { diskFileSystemProvider = this._register(new SandboxedDiskFileSystemProvider(mainProcessService, sharedProcessWorkerWorkbenchService, logService)); } else { + logService.info('[FileService]: NOT using sandbox ready file system provider'); diskFileSystemProvider = this._register(new ElectronFileSystemProvider(logService, nativeHostService, { legacyWatcher: this.configuration.legacyWatcher })); } fileService.registerProvider(Schemas.file, diskFileSystemProvider); diff --git a/src/vs/workbench/electron-sandbox/shared.desktop.main.ts b/src/vs/workbench/electron-sandbox/shared.desktop.main.ts index 255c03226f3..d4f8c7f2b46 100644 --- a/src/vs/workbench/electron-sandbox/shared.desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/shared.desktop.main.ts @@ -17,7 +17,6 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceInitializationPayload, reviveIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { ILoggerService, ILogService } from 'vs/platform/log/common/log'; import { NativeStorageService } from 'vs/platform/storage/electron-sandbox/storageService'; -import { Schemas } from 'vs/base/common/network'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -250,12 +249,8 @@ export abstract class SharedDesktopMain extends Disposable { // // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - - const connection = remoteAgentService.getConnection(); - if (connection) { - const remoteFileSystemProvider = this._register(new RemoteFileSystemProvider(remoteAgentService)); - fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider); - } + // Remote file system + this._register(RemoteFileSystemProvider.register(remoteAgentService, fileService, logService)); const payload = this.resolveWorkspaceInitializationPayload(environmentService); diff --git a/src/vs/workbench/services/assignment/common/assignmentService.ts b/src/vs/workbench/services/assignment/common/assignmentService.ts new file mode 100644 index 00000000000..8c290b81d8d --- /dev/null +++ b/src/vs/workbench/services/assignment/common/assignmentService.ts @@ -0,0 +1,127 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; +import type { IKeyValueStorage, IExperimentationTelemetry } from 'tas-client-umd'; +import { MementoObject, Memento } from 'vs/workbench/common/memento'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { ITelemetryData } from 'vs/base/common/actions'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IAssignmentService } from 'vs/platform/assignment/common/assignment'; +import { BaseAssignmentService } from 'vs/platform/assignment/common/assignmentService'; + +export const IWorkbenchAssignmentService = createDecorator<IWorkbenchAssignmentService>('WorkbenchAssignmentService'); + +export interface IWorkbenchAssignmentService extends IAssignmentService { + getCurrentExperiments(): Promise<string[] | undefined>; +} + +class MementoKeyValueStorage implements IKeyValueStorage { + private mementoObj: MementoObject; + constructor(private memento: Memento) { + this.mementoObj = memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + } + + async getValue<T>(key: string, defaultValue?: T | undefined): Promise<T | undefined> { + const value = await this.mementoObj[key]; + return value || defaultValue; + } + + setValue<T>(key: string, value: T): void { + this.mementoObj[key] = value; + this.memento.saveMemento(); + } +} + +class WorkbenchAssignmentServiceTelemetry implements IExperimentationTelemetry { + private _lastAssignmentContext: string | undefined; + constructor( + private telemetryService: ITelemetryService, + private productService: IProductService + ) { } + + get assignmentContext(): string[] | undefined { + return this._lastAssignmentContext?.split(';'); + } + + // __GDPR__COMMON__ "VSCode.ABExp.Features" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + // __GDPR__COMMON__ "abexp.assignmentcontext" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + setSharedProperty(name: string, value: string): void { + if (name === this.productService.tasConfig?.assignmentContextTelemetryPropertyName) { + this._lastAssignmentContext = value; + } + + this.telemetryService.setExperimentProperty(name, value); + } + + postEvent(eventName: string, props: Map<string, string>): void { + const data: ITelemetryData = {}; + for (const [key, value] of props.entries()) { + data[key] = value; + } + + /* __GDPR__ + "query-expfeature" : { + "ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog(eventName, data); + } +} + +export class WorkbenchAssignmentService extends BaseAssignmentService { + constructor( + @ITelemetryService private telemetryService: ITelemetryService, + @IStorageService storageService: IStorageService, + @IConfigurationService configurationService: IConfigurationService, + @IProductService productService: IProductService + ) { + + super(() => { + return telemetryService.getTelemetryInfo().then(telemetryInfo => { + return telemetryInfo.machineId; + }); + }, configurationService, productService, + new WorkbenchAssignmentServiceTelemetry(telemetryService, productService), + new MementoKeyValueStorage(new Memento('experiment.service.memento', storageService))); + } + + override async getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined> { + const result = await super.getTreatment<T>(name); + type TASClientReadTreatmentData = { + treatmentName: string; + treatmentValue: string; + }; + + type TASClientReadTreatmentClassification = { + treatmentValue: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', }; + treatmentName: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', }; + }; + + this.telemetryService.publicLog2<TASClientReadTreatmentData, TASClientReadTreatmentClassification>('tasClientReadTreatmentComplete', + { treatmentName: name, treatmentValue: JSON.stringify(result) }); + + return result; + } + + async getCurrentExperiments(): Promise<string[] | undefined> { + if (!this.tasClient) { + return undefined; + } + + if (!this.experimentsEnabled) { + return undefined; + } + + await this.tasClient; + + return (this.telemetry as WorkbenchAssignmentServiceTelemetry)?.assignmentContext; + } +} + +registerSingleton(IWorkbenchAssignmentService, WorkbenchAssignmentService, false); diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index f3756191951..8ef000ff354 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -558,8 +558,13 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat } private async initializeConfiguration(): Promise<void> { + mark('code/willInitUserConfiguration'); const { local, remote } = await this.initializeUserConfiguration(); + mark('code/didInitUserConfiguration'); + + mark('code/willInitWorkspaceConfiguration'); await this.loadConfiguration(local, remote); + mark('code/didInitWorkspaceConfiguration'); } private async initializeUserConfiguration(): Promise<{ local: ConfigurationModel, remote: ConfigurationModel }> { diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index 1fd445c0376..f62b2073920 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -11,7 +11,7 @@ import { IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifec import { isThenable } from 'vs/base/common/async'; import { LinkedList } from 'vs/base/common/linkedList'; import { createStyleSheet, createCSSRule, removeCSSRulesContainingSelector } from 'vs/base/browser/dom'; -import { IThemeService, IColorTheme, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { localize } from 'vs/nls'; import { isPromiseCanceledError } from 'vs/base/common/errors'; @@ -21,6 +21,7 @@ import { hash } from 'vs/base/common/hash'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { iconRegistry } from 'vs/base/common/codicons'; import { asArray, distinct } from 'vs/base/common/arrays'; +import { asCssVariableName } from 'vs/platform/theme/common/colorRegistry'; class DecorationRule { @@ -64,29 +65,29 @@ class DecorationRule { return --this._refCounter === 0; } - appendCSSRules(element: HTMLStyleElement, theme: IColorTheme): void { + appendCSSRules(element: HTMLStyleElement): void { if (!Array.isArray(this.data)) { - this._appendForOne(this.data, element, theme); + this._appendForOne(this.data, element); } else { - this._appendForMany(this.data, element, theme); + this._appendForMany(this.data, element); } } - private _appendForOne(data: IDecorationData, element: HTMLStyleElement, theme: IColorTheme): void { + private _appendForOne(data: IDecorationData, element: HTMLStyleElement): void { const { color, letter } = data; // label - createCSSRule(`.${this.itemColorClassName}`, `color: ${getColor(theme, color)};`, element); + createCSSRule(`.${this.itemColorClassName}`, `color: ${getColor(color)};`, element); if (ThemeIcon.isThemeIcon(letter)) { - this._createIconCSSRule(letter, color, element, theme); + this._createIconCSSRule(letter, color, element); } else if (letter) { - createCSSRule(`.${this.itemBadgeClassName}::after`, `content: "${letter}"; color: ${getColor(theme, color)};`, element); + createCSSRule(`.${this.itemBadgeClassName}::after`, `content: "${letter}"; color: ${getColor(color)};`, element); } } - private _appendForMany(data: IDecorationData[], element: HTMLStyleElement, theme: IColorTheme): void { + private _appendForMany(data: IDecorationData[], element: HTMLStyleElement): void { // label const { color } = data[0]; - createCSSRule(`.${this.itemColorClassName}`, `color: ${getColor(theme, color)};`, element); + createCSSRule(`.${this.itemColorClassName}`, `color: ${getColor(color)};`, element); // badge or icon let letters: string[] = []; @@ -102,23 +103,23 @@ class DecorationRule { } if (icon) { - this._createIconCSSRule(icon, color, element, theme); + this._createIconCSSRule(icon, color, element); } else { if (letters.length) { - createCSSRule(`.${this.itemBadgeClassName}::after`, `content: "${letters.join(', ')}"; color: ${getColor(theme, color)};`, element); + createCSSRule(`.${this.itemBadgeClassName}::after`, `content: "${letters.join(', ')}"; color: ${getColor(color)};`, element); } // bubble badge // TODO @misolori update bubble badge to adopt letter: ThemeIcon instead of unicode createCSSRule( `.${this.bubbleBadgeClassName}::after`, - `content: "\uea71"; color: ${getColor(theme, color)}; font-family: codicon; font-size: 14px; margin-right: 14px; opacity: 0.4;`, + `content: "\uea71"; color: ${getColor(color)}; font-family: codicon; font-size: 14px; margin-right: 14px; opacity: 0.4;`, element ); } } - private _createIconCSSRule(icon: ThemeIcon, color: string | undefined, element: HTMLStyleElement, theme: IColorTheme) { + private _createIconCSSRule(icon: ThemeIcon, color: string | undefined, element: HTMLStyleElement) { const index = icon.id.lastIndexOf('~'); const id = index < 0 ? icon.id : icon.id.substr(0, index); @@ -132,7 +133,7 @@ class DecorationRule { createCSSRule( `.${this.iconBadgeClassName}::after`, `content: "${String.fromCharCode(charCode)}"; - color: ${getColor(theme, color)}; + color: ${getColor(color)}; font-family: codicon; font-size: 16px; margin-right: 14px; @@ -157,10 +158,6 @@ class DecorationStyles { private readonly _decorationRules = new Map<string, DecorationRule>(); private readonly _dispoables = new DisposableStore(); - constructor(private readonly _themeService: IThemeService) { - this._themeService.onDidColorThemeChange(this._onThemeChange, this, this._dispoables); - } - dispose(): void { this._dispoables.dispose(); this._styleElement.remove(); @@ -178,7 +175,7 @@ class DecorationStyles { // new css rule rule = new DecorationRule(data, key); this._decorationRules.set(key, rule); - rule.appendCSSRules(this._styleElement, this._themeService.getColorTheme()); + rule.appendCSSRules(this._styleElement); } rule.acquire(); @@ -210,13 +207,6 @@ class DecorationStyles { } }; } - - private _onThemeChange(): void { - this._decorationRules.forEach(rule => { - rule.removeCSSRules(this._styleElement); - rule.appendCSSRules(this._styleElement, this._themeService.getColorTheme()); - }); - } } class FileDecorationChangeEvent implements IResourceDecorationChangeEvent { @@ -239,14 +229,8 @@ class DecorationDataRequest { ) { } } -function getColor(theme: IColorTheme, color: string | undefined) { - if (color) { - const foundColor = theme.getColor(color); - if (foundColor) { - return foundColor; - } - } - return 'inherit'; +function getColor(color: string | undefined) { + return color ? `var(${asCssVariableName(color)})` : 'inherit'; } type DecorationEntry = Map<IDecorationsProvider, DecorationDataRequest | IDecorationData | null>; @@ -265,10 +249,9 @@ export class DecorationsService implements IDecorationsService { private readonly _data: TernarySearchTree<URI, DecorationEntry>; constructor( - @IThemeService themeService: IThemeService, @IUriIdentityService uriIdentityService: IUriIdentityService, ) { - this._decorationStyles = new DecorationStyles(themeService); + this._decorationStyles = new DecorationStyles(); this._data = TernarySearchTree.forUris(key => uriIdentityService.extUri.ignorePathCasing(key)); this._onDidChangeDecorationsDelayed.event(event => { this._onDidChangeDecorations.fire(new FileDecorationChangeEvent(event)); }); diff --git a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts index 4c59c2f67d8..b89826f45ce 100644 --- a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts +++ b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts @@ -9,7 +9,6 @@ import { IDecorationsProvider, IDecorationData } from 'vs/workbench/services/dec import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import * as resources from 'vs/base/common/resources'; -import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { mock } from 'vs/base/test/common/mock'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; @@ -23,7 +22,6 @@ suite('DecorationsService', function () { service.dispose(); } service = new DecorationsService( - new TestThemeService(), new class extends mock<IUriIdentityService>() { override extUri = resources.extUri; } diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index 0a1bc261430..0f49150fc30 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -266,8 +266,7 @@ export class SimpleFileDialog { } } - // eslint-disable-next-line no-async-promise-executor - return new Promise<URI | undefined>(async (resolve) => { + return new Promise<URI | undefined>((resolve) => { this.filePickBox = this.quickInputService.createQuickPick<FileQuickPickItem>(); this.busy = true; this.filePickBox.matchOnLabel = false; @@ -396,13 +395,14 @@ export class SimpleFileDialog { this.filePickBox.show(); this.contextKey.set(true); - await this.updateItems(homedir, true, this.trailing); - if (this.trailing) { - this.filePickBox.valueSelection = [this.filePickBox.value.length - this.trailing.length, this.filePickBox.value.length - ext.length]; - } else { - this.filePickBox.valueSelection = [this.filePickBox.value.length, this.filePickBox.value.length]; - } - this.busy = false; + this.updateItems(homedir, true, this.trailing).then(() => { + if (this.trailing) { + this.filePickBox.valueSelection = [this.filePickBox.value.length - this.trailing.length, this.filePickBox.value.length - ext.length]; + } else { + this.filePickBox.valueSelection = [this.filePickBox.value.length, this.filePickBox.value.length]; + } + this.busy = false; + }); }); } diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index d406b6c4699..5445b7ff1d2 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -164,6 +164,38 @@ suite('EditorService', () => { didCloseEditorListener.dispose(); }); + test('openEditor() - multiple calls are cancelled and indicated as such', async () => { + const [, service] = await createEditorService(); + + let input = new TestFileEditorInput(URI.parse('my://resource-basics'), TEST_EDITOR_INPUT_ID); + let otherInput = new TestFileEditorInput(URI.parse('my://resource2-basics'), TEST_EDITOR_INPUT_ID); + + let activeEditorChangeEventCounter = 0; + const activeEditorChangeListener = service.onDidActiveEditorChange(() => { + activeEditorChangeEventCounter++; + }); + + let visibleEditorChangeEventCounter = 0; + const visibleEditorChangeListener = service.onDidVisibleEditorsChange(() => { + visibleEditorChangeEventCounter++; + }); + + const editorP1 = service.openEditor(input, { pinned: true }); + const editorP2 = service.openEditor(otherInput, { pinned: true }); + + const editor1 = await editorP1; + assert.strictEqual(editor1, undefined); + + const editor2 = await editorP2; + assert.strictEqual(editor2?.input, otherInput); + + assert.strictEqual(activeEditorChangeEventCounter, 1); + assert.strictEqual(visibleEditorChangeEventCounter, 1); + + activeEditorChangeListener.dispose(); + visibleEditorChangeListener.dispose(); + }); + test('openEditor() - locked groups', async () => { disposables.add(registerTestFileEditor()); diff --git a/src/vs/workbench/services/experiment/common/experimentService.ts b/src/vs/workbench/services/experiment/common/experimentService.ts deleted file mode 100644 index c9d0c8d54a5..00000000000 --- a/src/vs/workbench/services/experiment/common/experimentService.ts +++ /dev/null @@ -1,313 +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'; import * as platform from 'vs/base/common/platform'; -import type { IKeyValueStorage, IExperimentationTelemetry, IExperimentationFilterProvider, ExperimentationService as TASClient } from 'tas-client-umd'; -import { MementoObject, Memento } from 'vs/workbench/common/memento'; -import { ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { ITelemetryData } from 'vs/base/common/actions'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IProductService } from 'vs/platform/product/common/productService'; - -export const ITASExperimentService = createDecorator<ITASExperimentService>('TASExperimentService'); - -export interface ITASExperimentService { - readonly _serviceBrand: undefined; - getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined>; - getCurrentExperiments(): Promise<string[] | undefined>; -} - -const storageKey = 'VSCode.ABExp.FeatureData'; -const refetchInterval = 0; // no polling - -class MementoKeyValueStorage implements IKeyValueStorage { - private mementoObj: MementoObject; - constructor(private memento: Memento) { - this.mementoObj = memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); - } - - async getValue<T>(key: string, defaultValue?: T | undefined): Promise<T | undefined> { - const value = await this.mementoObj[key]; - return value || defaultValue; - } - - setValue<T>(key: string, value: T): void { - this.mementoObj[key] = value; - this.memento.saveMemento(); - } -} - -class ExperimentServiceTelemetry implements IExperimentationTelemetry { - private _lastAssignmentContext: string | undefined; - constructor( - private telemetryService: ITelemetryService, - private productService: IProductService - ) { } - - get assignmentContext(): string[] | undefined { - return this._lastAssignmentContext?.split(';'); - } - - // __GDPR__COMMON__ "VSCode.ABExp.Features" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - // __GDPR__COMMON__ "abexp.assignmentcontext" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - setSharedProperty(name: string, value: string): void { - if (name === this.productService.tasConfig?.assignmentContextTelemetryPropertyName) { - this._lastAssignmentContext = value; - } - - this.telemetryService.setExperimentProperty(name, value); - } - - postEvent(eventName: string, props: Map<string, string>): void { - const data: ITelemetryData = {}; - for (const [key, value] of props.entries()) { - data[key] = value; - } - - /* __GDPR__ - "query-expfeature" : { - "ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog(eventName, data); - } -} - -class ExperimentServiceFilterProvider implements IExperimentationFilterProvider { - constructor( - private version: string, - private appName: string, - private machineId: string, - private targetPopulation: TargetPopulation - ) { } - - getFilterValue(filter: string): string | null { - switch (filter) { - case Filters.ApplicationVersion: - return this.version; // productService.version - case Filters.Build: - return this.appName; // productService.nameLong - case Filters.ClientId: - return this.machineId; - case Filters.Language: - return platform.language; - case Filters.ExtensionName: - return 'vscode-core'; // always return vscode-core for exp service - case Filters.TargetPopulation: - return this.targetPopulation; - default: - return ''; - } - } - - getFilters(): Map<string, any> { - let filters: Map<string, any> = new Map<string, any>(); - let filterValues = Object.values(Filters); - for (let value of filterValues) { - filters.set(value, this.getFilterValue(value)); - } - - return filters; - } -} - -/* -Based upon the official VSCode currently existing filters in the -ExP backend for the VSCode cluster. -https://experimentation.visualstudio.com/Analysis%20and%20Experimentation/_git/AnE.ExP.TAS.TachyonHost.Configuration?path=%2FConfigurations%2Fvscode%2Fvscode.json&version=GBmaster -"X-MSEdge-Market": "detection.market", -"X-FD-Corpnet": "detection.corpnet", -"X-VSCode–AppVersion": "appversion", -"X-VSCode-Build": "build", -"X-MSEdge-ClientId": "clientid", -"X-VSCode-ExtensionName": "extensionname", -"X-VSCode-TargetPopulation": "targetpopulation", -"X-VSCode-Language": "language" -*/ - -enum Filters { - /** - * The market in which the extension is distributed. - */ - Market = 'X-MSEdge-Market', - - /** - * The corporation network. - */ - CorpNet = 'X-FD-Corpnet', - - /** - * Version of the application which uses experimentation service. - */ - ApplicationVersion = 'X-VSCode-AppVersion', - - /** - * Insiders vs Stable. - */ - Build = 'X-VSCode-Build', - - /** - * Client Id which is used as primary unit for the experimentation. - */ - ClientId = 'X-MSEdge-ClientId', - - /** - * Extension header. - */ - ExtensionName = 'X-VSCode-ExtensionName', - - /** - * The language in use by VS Code - */ - Language = 'X-VSCode-Language', - - /** - * The target population. - * This is used to separate internal, early preview, GA, etc. - */ - TargetPopulation = 'X-VSCode-TargetPopulation', -} - -enum TargetPopulation { - Team = 'team', - Internal = 'internal', - Insiders = 'insider', - Public = 'public', -} - -export class ExperimentService implements ITASExperimentService { - _serviceBrand: undefined; - private tasClient: Promise<TASClient> | undefined; - private telemetry: ExperimentServiceTelemetry | undefined; - private static MEMENTO_ID = 'experiment.service.memento'; - private networkInitialized = false; - - private overrideInitDelay: Promise<void>; - - private get experimentsEnabled(): boolean { - return this.configurationService.getValue('workbench.enableExperiments') === true; - } - - constructor( - @ITelemetryService private telemetryService: ITelemetryService, - @IStorageService private storageService: IStorageService, - @IConfigurationService private configurationService: IConfigurationService, - @IProductService private productService: IProductService - ) { - - if (productService.tasConfig && this.experimentsEnabled && this.telemetryService.telemetryLevel === TelemetryLevel.USAGE) { - this.tasClient = this.setupTASClient(); - } - - // For development purposes, configure the delay until tas local tas treatment ovverrides are available - const overrideDelaySetting = this.configurationService.getValue('experiments.overrideDelay'); - const overrideDelay = typeof overrideDelaySetting === 'number' ? overrideDelaySetting : 0; - this.overrideInitDelay = new Promise(resolve => setTimeout(resolve, overrideDelay)); - } - - async getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined> { - // For development purposes, allow overriding tas assignments to test variants locally. - await this.overrideInitDelay; - const override = this.configurationService.getValue<T>('experiments.override.' + name); - if (override !== undefined) { - type TAASClientOverrideTreatmentData = { treatmentName: string; }; - type TAASClientOverrideTreatmentClassification = { treatmentName: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', }; }; - this.telemetryService.publicLog2<TAASClientOverrideTreatmentData, TAASClientOverrideTreatmentClassification>('tasClientOverrideTreatment', { treatmentName: name, }); - return override; - } - - const startSetup = Date.now(); - - if (!this.tasClient) { - return undefined; - } - - if (!this.experimentsEnabled) { - return undefined; - } - - let result: T | undefined; - const client = await this.tasClient; - if (this.networkInitialized) { - result = client.getTreatmentVariable<T>('vscode', name); - } else { - result = await client.getTreatmentVariableAsync<T>('vscode', name, true); - } - - type TAASClientReadTreatmentData = { - treatmentName: string; - treatmentValue: string; - readTime: number; - }; - - type TAASClientReadTreatmentCalssification = { - treatmentValue: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', }; - treatmentName: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', }; - readTime: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true }; - }; - this.telemetryService.publicLog2<TAASClientReadTreatmentData, TAASClientReadTreatmentCalssification>('tasClientReadTreatmentComplete', - { readTime: Date.now() - startSetup, treatmentName: name, treatmentValue: JSON.stringify(result) }); - - return result; - } - - async getCurrentExperiments(): Promise<string[] | undefined> { - if (!this.tasClient) { - return undefined; - } - - if (!this.experimentsEnabled) { - return undefined; - } - - await this.tasClient; - - return this.telemetry?.assignmentContext; - } - - private async setupTASClient(): Promise<TASClient> { - const startSetup = Date.now(); - const telemetryInfo = await this.telemetryService.getTelemetryInfo(); - const targetPopulation = telemetryInfo.msftInternal ? TargetPopulation.Internal : (this.productService.quality === 'stable' ? TargetPopulation.Public : TargetPopulation.Insiders); - const machineId = telemetryInfo.machineId; - const filterProvider = new ExperimentServiceFilterProvider( - this.productService.version, - this.productService.nameLong, - machineId, - targetPopulation - ); - - const keyValueStorage = new MementoKeyValueStorage(new Memento(ExperimentService.MEMENTO_ID, this.storageService)); - - this.telemetry = new ExperimentServiceTelemetry(this.telemetryService, this.productService); - - const tasConfig = this.productService.tasConfig!; - const tasClient = new (await import('tas-client-umd')).ExperimentationService({ - filterProviders: [filterProvider], - telemetry: this.telemetry, - storageKey: storageKey, - keyValueStorage: keyValueStorage, - featuresTelemetryPropertyName: tasConfig.featuresTelemetryPropertyName, - assignmentContextTelemetryPropertyName: tasConfig.assignmentContextTelemetryPropertyName, - telemetryEventName: tasConfig.telemetryEventName, - endpoint: tasConfig.endpoint, - refetchInterval: refetchInterval, - }); - - await tasClient.initializePromise; - - tasClient.initialFetch.then(() => this.networkInitialized = true); - - type TAASClientSetupData = { setupTime: number; }; - type TAASClientSetupCalssification = { setupTime: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true }; }; - this.telemetryService.publicLog2<TAASClientSetupData, TAASClientSetupCalssification>('tasClientSetupComplete', { setupTime: Date.now() - startSetup }); - - return tasClient; - } -} - -registerSingleton(ITASExperimentService, ExperimentService, false); diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index 659a93df4ec..1a8d82ad570 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -93,7 +93,11 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost const forceHTTPS = (location.protocol === 'https:'); - if (this._environmentService.options && this._environmentService.options.__uniqueWebWorkerExtensionHostOrigin) { + let uniqueWebWorkerExtensionHostOrigin = true; + if (this._environmentService.options && typeof this._environmentService.options.__uniqueWebWorkerExtensionHostOrigin !== 'undefined') { + uniqueWebWorkerExtensionHostOrigin = this._environmentService.options.__uniqueWebWorkerExtensionHostOrigin; + } + if (uniqueWebWorkerExtensionHostOrigin) { const webEndpointUrlTemplate = this._productService.webEndpointUrlTemplate; const commit = this._productService.commit; const quality = this._productService.quality; diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index aedda47bd66..5a571ed0f1f 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -190,7 +190,9 @@ export abstract class AbstractExtensionService extends Disposable implements IEx // help the file service to activate providers by activating extensions by file system event this._register(this._fileService.onWillActivateFileSystemProvider(e => { - e.join(this.activateByEvent(`onFileSystem:${e.scheme}`)); + if (e.scheme !== Schemas.vscodeRemote) { + e.join(this.activateByEvent(`onFileSystem:${e.scheme}`)); + } })); this._registry = new ExtensionDescriptionRegistry([]); diff --git a/src/vs/workbench/services/files/electron-sandbox/diskFileSystemProvider.ts b/src/vs/workbench/services/files/electron-sandbox/diskFileSystemProvider.ts index 57ece36f8f0..3d84135a2d4 100644 --- a/src/vs/workbench/services/files/electron-sandbox/diskFileSystemProvider.ts +++ b/src/vs/workbench/services/files/electron-sandbox/diskFileSystemProvider.ts @@ -29,11 +29,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability { - private readonly provider = this._register(new class extends IPCFileSystemProvider { - constructor(mainProcessService: IMainProcessService) { - super(mainProcessService.getChannel('localFilesystem')); - } - }(this.mainProcessService)); + private readonly provider = this._register(new IPCFileSystemProvider(this.mainProcessService.getChannel('localFilesystem'), { pathCaseSensitive: isLinux, trash: true })); constructor( private readonly mainProcessService: IMainProcessService, @@ -54,26 +50,9 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple //#region File Capabilities - readonly onDidChangeCapabilities: Event<void> = Event.None; + get onDidChangeCapabilities(): Event<void> { return this.provider.onDidChangeCapabilities; } - private _capabilities: FileSystemProviderCapabilities | undefined; - get capabilities(): FileSystemProviderCapabilities { - if (!this._capabilities) { - this._capabilities = - FileSystemProviderCapabilities.FileReadWrite | - FileSystemProviderCapabilities.FileOpenReadWriteClose | - FileSystemProviderCapabilities.FileReadStream | - FileSystemProviderCapabilities.FileFolderCopy | - FileSystemProviderCapabilities.Trash | - FileSystemProviderCapabilities.FileWriteUnlock; - - if (isLinux) { - this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive; - } - } - - return this._capabilities; - } + get capabilities(): FileSystemProviderCapabilities { return this.provider.capabilities; } //#endregion diff --git a/src/vs/workbench/services/files/electron-sandbox/parcelWatcherService.ts b/src/vs/workbench/services/files/electron-sandbox/parcelWatcherService.ts index 61c268166a9..4edab669348 100644 --- a/src/vs/workbench/services/files/electron-sandbox/parcelWatcherService.ts +++ b/src/vs/workbench/services/files/electron-sandbox/parcelWatcherService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { DisposableStore } from 'vs/base/common/lifecycle'; -import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; +import { getDelayedChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; import { AbstractWatcherService, IDiskFileChange, ILogMessage, IWatcherService } from 'vs/platform/files/common/watcher'; import { ISharedProcessWorkerWorkbenchService } from 'vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessWorkerWorkbenchService'; @@ -22,13 +22,24 @@ export class ParcelFileWatcher extends AbstractWatcherService { } protected override createService(disposables: DisposableStore): IWatcherService { + return ProxyChannel.toService<IWatcherService>(getDelayedChannel((async () => { - // Acquire parcel watcher via shared process worker - const watcherChannel = this.sharedProcessWorkerWorkbenchService.createWorkerChannel({ - moduleId: 'vs/platform/files/node/watcher/parcel/watcherApp', - type: 'watcherServiceParcelSharedProcess' - }, 'watcher').channel; + // Acquire parcel watcher via shared process worker + const { client, onDidTerminate } = await this.sharedProcessWorkerWorkbenchService.createWorker({ + moduleId: 'vs/platform/files/node/watcher/parcel/watcherApp', + type: 'watcherServiceParcelSharedProcess' + }); - return ProxyChannel.toService<IWatcherService>(watcherChannel); + // React on unexpected termination of the watcher process + // We never expect the watcher to terminate by its own, + // so if that happens we want to restart the watcher. + onDidTerminate.then(({ reason }) => { + if (reason) { + this.onError(`terminated by itself with code ${reason.code}, signal: ${reason.signal}`); + } + }); + + return client.getChannel('watcher'); + })())); } } diff --git a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts index fcd730bcdcc..a760a0d2e8a 100644 --- a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts +++ b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts @@ -169,7 +169,7 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi return AutoSaveMode.ON_WINDOW_CHANGE; } - if (this.configuredAutoSaveDelay && this.configuredAutoSaveDelay > 0) { + if (typeof this.configuredAutoSaveDelay === 'number' && this.configuredAutoSaveDelay >= 0) { return this.configuredAutoSaveDelay <= 1000 ? AutoSaveMode.AFTER_SHORT_DELAY : AutoSaveMode.AFTER_LONG_DELAY; } @@ -178,7 +178,7 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi getAutoSaveConfiguration(): IAutoSaveConfiguration { return { - autoSaveDelay: this.configuredAutoSaveDelay && this.configuredAutoSaveDelay > 0 ? this.configuredAutoSaveDelay : undefined, + autoSaveDelay: typeof this.configuredAutoSaveDelay === 'number' && this.configuredAutoSaveDelay >= 0 ? this.configuredAutoSaveDelay : undefined, autoSaveFocusChange: !!this.configuredAutoSaveOnFocusChange, autoSaveApplicationChange: !!this.configuredAutoSaveOnWindowChange }; diff --git a/src/vs/workbench/services/issue/electron-sandbox/issueService.ts b/src/vs/workbench/services/issue/electron-sandbox/issueService.ts index 8e29675c52c..cf004784fe9 100644 --- a/src/vs/workbench/services/issue/electron-sandbox/issueService.ts +++ b/src/vs/workbench/services/issue/electron-sandbox/issueService.ts @@ -16,7 +16,7 @@ import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/enviro import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { platform } from 'vs/base/common/process'; import { IProductService } from 'vs/platform/product/common/productService'; -import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; +import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; import { registerMainProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; @@ -32,7 +32,7 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IProductService private readonly productService: IProductService, - @ITASExperimentService private readonly experimentService: ITASExperimentService, + @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService, @IAuthenticationService private readonly authenticationService: IAuthenticationService ) { } diff --git a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts index ba593fe6731..d50305d48ab 100644 --- a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts @@ -9,13 +9,17 @@ import { AbstractLifecycleService } from 'vs/workbench/services/lifecycle/common import { localize } from 'vs/nls'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { addDisposableListener } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType } from 'vs/base/browser/dom'; import { IStorageService } from 'vs/platform/storage/common/storage'; export class BrowserLifecycleService extends AbstractLifecycleService { - private beforeUnloadDisposable: IDisposable | undefined = undefined; - private disableUnloadHandling = false; + private beforeUnloadListener: IDisposable | undefined = undefined; + + private disableBeforeUnloadVeto = false; + + private didBeforeUnload = false; + private didUnload = false; constructor( @ILogService logService: ILogService, @@ -28,27 +32,71 @@ export class BrowserLifecycleService extends AbstractLifecycleService { private registerListeners(): void { - // beforeUnload - this.beforeUnloadDisposable = addDisposableListener(window, 'beforeunload', (e: BeforeUnloadEvent) => this.onBeforeUnload(e)); + // Listen to `pageshow` to handle unsupported `persisted: true` cases + this._register(addDisposableListener(window, EventType.PAGE_SHOW, (e: PageTransitionEvent) => this.onLoad(e))); + + // Listen to `beforeUnload` to support to veto + this.beforeUnloadListener = addDisposableListener(window, EventType.BEFORE_UNLOAD, (e: BeforeUnloadEvent) => this.onBeforeUnload(e)); + + // Listen to `pagehide` to support orderly shutdown + // We explicitly do not listen to `unload` event + // which would disable certain browser caching. + // We currently do not handle the `persisted` property + // (https://github.com/microsoft/vscode/issues/136216) + this._register(addDisposableListener(window, EventType.PAGE_HIDE, () => this.onUnload())); + } + + private onLoad(event: PageTransitionEvent): void { + + // We only really care about page-show events + // where the browser indicates to us that the + // page was restored from cache and not freshly + // loaded. + const wasRestoredFromCache = event.persisted; + if (!wasRestoredFromCache) { + return; + } + + // We only really care about `persisted` page-show + // events if there is a chance that we were unloaded + // before and now potentially have a disposed workbench + // that is non-functional. + // To be on the safe side, we ignore this event in any + // other cases to not accidentally reload the workbench. + const handleLoadEvent = this.didBeforeUnload; + if (!handleLoadEvent) { + return; + } + + // At this point, we know that the page was restored from + // cache even though it was potentially unloaded before, + // so in order to get back to a functional workbench, we + // currently can only reload the window + // Docs: https://web.dev/bfcache/#optimize-your-pages-for-bfcache + // Refs: https://github.com/microsoft/vscode/issues/136035 + this.withExpectedShutdown({ disableShutdownHandling: true }, () => window.location.reload()); } private onBeforeUnload(event: BeforeUnloadEvent): void { - if (this.disableUnloadHandling) { - this.logService.info('[lifecycle] onBeforeUnload disabled, ignoring once'); - this.disableUnloadHandling = false; + // Unload without veto support + if (this.disableBeforeUnloadVeto) { + this.logService.info('[lifecycle] onBeforeUnload triggered and handled without veto support'); - return; // ignore unload handling only once + this.doShutdown(); } - this.logService.info('[lifecycle] onBeforeUnload triggered'); + // Unload with veto support + else { + this.logService.info('[lifecycle] onBeforeUnload triggered and handled with veto support'); - this.doShutdown(() => { + this.doShutdown(() => this.vetoBeforeUnload(event)); + } + } - // Veto handling - event.preventDefault(); - event.returnValue = localize('lifecycleVeto', "Changes that you made may not be saved. Please check press 'Cancel' and try again."); - }); + private vetoBeforeUnload(event: BeforeUnloadEvent): void { + event.preventDefault(); + event.returnValue = localize('lifecycleVeto', "Changes that you made may not be saved. Please check press 'Cancel' and try again."); } withExpectedShutdown(reason: ShutdownReason): void; @@ -60,13 +108,13 @@ export class BrowserLifecycleService extends AbstractLifecycleService { this.shutdownReason = reason; } - // Shutdown handling disabled for duration of callback + // Veto handling disabled for duration of callback else { - this.disableUnloadHandling = true; + this.disableBeforeUnloadVeto = true; try { callback?.(); } finally { - this.disableUnloadHandling = false; + this.disableBeforeUnloadVeto = false; } } } @@ -74,22 +122,25 @@ export class BrowserLifecycleService extends AbstractLifecycleService { shutdown(): void { this.logService.info('[lifecycle] shutdown triggered'); - // Remove `beforeunload` listener that would prevent shutdown - this.beforeUnloadDisposable?.dispose(); + // An explicit shutdown renders `beforeUnload` event + // handling disabled from here on + this.beforeUnloadListener?.dispose(); // Handle shutdown without veto support this.doShutdown(); } - private doShutdown(handleVeto?: () => void): void { + private doShutdown(vetoShutdown?: () => void): void { const logService = this.logService; + this.didBeforeUnload = true; + let veto = false; // Before Shutdown this._onBeforeShutdown.fire({ veto(value, id) { - if (typeof handleVeto === 'function') { + if (typeof vetoShutdown === 'function') { if (value instanceof Promise) { logService.error(`[lifecycle] Long running operations before shutdown are unsupported in the web (id: ${id})`); @@ -107,13 +158,24 @@ export class BrowserLifecycleService extends AbstractLifecycleService { }); // Veto: handle if provided - if (veto && typeof handleVeto === 'function') { - handleVeto(); - - return; + if (veto && typeof vetoShutdown === 'function') { + return vetoShutdown(); } - // No Veto: continue with willShutdown + // No veto, continue to shutdown + return this.onUnload(); + } + + private onUnload(): void { + if (this.didUnload) { + return; // only once + } + + this.didUnload = true; + + const logService = this.logService; + + // First indicate will-shutdown this._onWillShutdown.fire({ join(promise, id) { logService.error(`[lifecycle] Long running operations during shutdown are unsupported in the web (id: ${id})`); @@ -121,7 +183,7 @@ export class BrowserLifecycleService extends AbstractLifecycleService { reason: ShutdownReason.QUIT }); - // Finally end with didShutdown + // Finally end with did-shutdown this._onDidShutdown.fire(); } } diff --git a/src/vs/workbench/services/remote/common/remoteAgentFileSystemChannel.ts b/src/vs/workbench/services/remote/common/remoteAgentFileSystemChannel.ts index 6ef6d5aae2d..e718e985d86 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentFileSystemChannel.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentFileSystemChannel.ts @@ -3,22 +3,54 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { getErrorMessage } from 'vs/base/common/errors'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; import { OperatingSystem } from 'vs/base/common/platform'; +import { IFileService } from 'vs/platform/files/common/files'; import { IPCFileSystemProvider } from 'vs/platform/files/common/ipcFileSystemProvider'; -import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; +import { IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; export const REMOTE_FILE_SYSTEM_CHANNEL_NAME = 'remoteFilesystem'; export class RemoteFileSystemProvider extends IPCFileSystemProvider { - constructor(remoteAgentService: IRemoteAgentService) { - super(remoteAgentService.getConnection()!.getChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME)); + static register(remoteAgentService: IRemoteAgentService, fileService: IFileService, logService: ILogService): IDisposable { + const connection = remoteAgentService.getConnection(); + if (!connection) { + return Disposable.None; + } - // Initially assume case sensitivity until remote environment is resolved - this.setCaseSensitive(true); - (async () => { - const remoteAgentEnvironment = await remoteAgentService.getEnvironment(); - this.setCaseSensitive(remoteAgentEnvironment?.os === OperatingSystem.Linux); + const disposables = new DisposableStore(); + + const environmentPromise = (async () => { + try { + const environment = await remoteAgentService.getRawEnvironment(); + if (environment) { + // Register remote fsp even before it is asked to activate + // because, some features (configuration) wait for its + // registration before making fs calls. + fileService.registerProvider(Schemas.vscodeRemote, disposables.add(new RemoteFileSystemProvider(environment, connection))); + } else { + logService.error('Cannot register remote filesystem provider. Remote environment doesnot exist.'); + } + } catch (error) { + logService.error('Cannot register remote filesystem provider. Error while fetching remote environment.', getErrorMessage(error)); + } })(); + + disposables.add(fileService.onWillActivateFileSystemProvider(e => { + if (e.scheme === Schemas.vscodeRemote) { + e.join(environmentPromise); + } + })); + + return disposables; + } + + private constructor(remoteAgentEnvironment: IRemoteAgentEnvironment, connection: IRemoteAgentConnection) { + super(connection.getChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME), { pathCaseSensitive: remoteAgentEnvironment.os === OperatingSystem.Linux }); } } diff --git a/src/vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessWorkerWorkbenchService.ts b/src/vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessWorkerWorkbenchService.ts index 4928c9d8419..b9b89e84f70 100644 --- a/src/vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessWorkerWorkbenchService.ts +++ b/src/vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessWorkerWorkbenchService.ts @@ -8,16 +8,30 @@ import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/ import { ISharedProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { Client as MessagePortClient } from 'vs/base/parts/ipc/common/ipc.mp'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ipcSharedProcessWorkerChannelName, ISharedProcessWorkerProcess, ISharedProcessWorkerService } from 'vs/platform/sharedProcess/common/sharedProcessWorkerService'; -import { getDelayedChannel, IChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; +import { IOnDidTerminateSharedProcessWorkerProcess, ipcSharedProcessWorkerChannelName, ISharedProcessWorkerProcess, ISharedProcessWorkerService } from 'vs/platform/sharedProcess/common/sharedProcessWorkerService'; +import { IPCClient, ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; import { generateUuid } from 'vs/base/common/uuid'; import { acquirePort } from 'vs/base/parts/ipc/electron-sandbox/ipc.mp'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; export const ISharedProcessWorkerWorkbenchService = createDecorator<ISharedProcessWorkerWorkbenchService>('sharedProcessWorkerWorkbenchService'); -export interface IWorkerChannel extends IDisposable { - channel: IChannel; +export interface ISharedProcessWorker extends IDisposable { + + /** + * A IPC client to communicate to the worker process. + */ + client: IPCClient<string>; + + /** + * A promise that resolves to an object once the + * worker process terminates, giving information + * how the process terminated. + * + * This can be used to figure out whether the worker + * should be restarted in case of an unexpected + * termination. + */ + onDidTerminate: Promise<IOnDidTerminateSharedProcessWorkerProcess>; } export interface ISharedProcessWorkerWorkbenchService { @@ -41,13 +55,12 @@ export interface ISharedProcessWorkerWorkbenchService { * window and the communication channel allows to dynamically update the processes * after the fact. * - * @param process information around the process to fork - * @param channelName the name of the channel the process will respond to + * @param process information around the process to fork as worker * - * @returns the worker channel to communicate with. Provides a `dispose` method that + * @returns the worker IPC client to communicate with. Provides a `dispose` method that * allows to terminate the worker if needed. */ - createWorkerChannel(process: ISharedProcessWorkerProcess, channelName: string): IWorkerChannel; + createWorker(process: ISharedProcessWorkerProcess): Promise<ISharedProcessWorker>; } export class SharedProcessWorkerWorkbenchService extends Disposable implements ISharedProcessWorkerWorkbenchService { @@ -71,21 +84,9 @@ export class SharedProcessWorkerWorkbenchService extends Disposable implements I super(); } - createWorkerChannel(process: ISharedProcessWorkerProcess, channelName: string): IWorkerChannel { - const cts = new CancellationTokenSource(); + async createWorker(process: ISharedProcessWorkerProcess): Promise<ISharedProcessWorker> { + this.logService.trace('Renderer->SharedProcess#createWorker'); - return { - channel: getDelayedChannel(this.doCreateWorkerChannel(process, channelName, cts.token)), - dispose: () => cts.dispose(true) - }; - } - - private async doCreateWorkerChannel(process: ISharedProcessWorkerProcess, channelName: string, token: CancellationToken): Promise<IChannel> { - this.logService.trace('Renderer->SharedProcess#createWorkerChannel'); - - // Dispose when cancelled - const disposables = new DisposableStore(); - token.onCancellationRequested(() => disposables.dispose()); // Get ready to acquire the message port from the shared process worker const nonce = generateUuid(); @@ -94,12 +95,13 @@ export class SharedProcessWorkerWorkbenchService extends Disposable implements I // Actually talk with the shared process service // to create a new process from a worker - this.sharedProcessWorkerService.createWorker({ + const onDidTerminate = this.sharedProcessWorkerService.createWorker({ process, reply: { windowId: this.windowId, channel: responseChannel, nonce } }); // Dispose worker upon disposal via shared process service + const disposables = new DisposableStore(); disposables.add(toDisposable(() => { this.logService.trace('Renderer->SharedProcess#disposeWorker', process); @@ -110,11 +112,9 @@ export class SharedProcessWorkerWorkbenchService extends Disposable implements I })); const port = await portPromise; - + const client = disposables.add(new MessagePortClient(port, `window:${this.windowId},module:${process.moduleId}`)); this.logService.trace('Renderer->SharedProcess#createWorkerChannel: connection established'); - const client = disposables.add(new MessagePortClient(port, `window:${this.windowId},module:${process.moduleId}`)); - - return client.getChannel(channelName); + return { client, onDidTerminate, dispose: () => disposables.dispose() }; } } diff --git a/src/vs/workbench/services/textfile/test/node/encoding/encoding.integrationTest.ts b/src/vs/workbench/services/textfile/test/node/encoding/encoding.integrationTest.ts new file mode 100644 index 00000000000..e7a3568863a --- /dev/null +++ b/src/vs/workbench/services/textfile/test/node/encoding/encoding.integrationTest.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 * as assert from 'assert'; +import * as terminalEncoding from 'vs/base/node/terminalEncoding'; + +suite('Encoding', () => { + + test('resolve terminal encoding (detect)', async function () { + const enc = await terminalEncoding.resolveTerminalEncoding(); + assert.ok(enc.length > 0); + }); +}); diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index 5b916d080be..a651107df9e 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -9,10 +9,10 @@ import * as Paths from 'vs/base/common/path'; import * as resources from 'vs/base/common/resources'; import * as Json from 'vs/base/common/json'; import { ExtensionData, IThemeExtensionPoint, IWorkbenchFileIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import { IFileService } from 'vs/platform/files/common/files'; import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages'; import { asCSSUrl } from 'vs/base/browser/dom'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader'; export class FileIconThemeData implements IWorkbenchFileIconTheme { @@ -42,15 +42,15 @@ export class FileIconThemeData implements IWorkbenchFileIconTheme { this.hidesExplorerArrows = false; } - public ensureLoaded(fileService: IFileService): Promise<string | undefined> { + public ensureLoaded(fileService: IExtensionResourceLoaderService): Promise<string | undefined> { return !this.isLoaded ? this.load(fileService) : Promise.resolve(this.styleSheetContent); } - public reload(fileService: IFileService): Promise<string | undefined> { + public reload(fileService: IExtensionResourceLoaderService): Promise<string | undefined> { return this.load(fileService); } - private load(fileService: IFileService): Promise<string | undefined> { + private load(fileService: IExtensionResourceLoaderService): Promise<string | undefined> { if (!this.location) { return Promise.resolve(this.styleSheetContent); } @@ -197,10 +197,10 @@ interface IconThemeDocument extends IconsAssociation { hidesExplorerArrows?: boolean; } -function _loadIconThemeDocument(fileService: IFileService, location: URI): Promise<IconThemeDocument> { - return fileService.readFile(location).then((content) => { +function _loadIconThemeDocument(fileService: IExtensionResourceLoaderService, location: URI): Promise<IconThemeDocument> { + return fileService.readExtensionResource(location).then((content) => { let errors: Json.ParseError[] = []; - let contentValue = Json.parse(content.value.toString(), errors); + let contentValue = Json.parse(content, errors); if (errors.length > 0) { return Promise.reject(new Error(nls.localize('error.cannotparseicontheme', "Problems parsing file icons file: {0}", errors.map(e => getParseErrorMessage(e.error)).join(', ')))); } else if (Json.getNodeType(contentValue) !== 'object') { diff --git a/src/vs/workbench/services/themes/browser/productIconThemeData.ts b/src/vs/workbench/services/themes/browser/productIconThemeData.ts index 6f6bddc572e..f9f68c129fd 100644 --- a/src/vs/workbench/services/themes/browser/productIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/productIconThemeData.ts @@ -9,7 +9,6 @@ import * as Paths from 'vs/base/common/path'; import * as resources from 'vs/base/common/resources'; import * as Json from 'vs/base/common/json'; import { ExtensionData, IThemeExtensionPoint, IWorkbenchProductIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import { IFileService } from 'vs/platform/files/common/files'; import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages'; import { asCSSUrl } from 'vs/base/browser/dom'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -19,6 +18,7 @@ import { isString } from 'vs/base/common/types'; import { ILogService } from 'vs/platform/log/common/log'; import { getIconRegistry } from 'vs/platform/theme/common/iconRegistry'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader'; export const DEFAULT_PRODUCT_ICON_THEME_ID = ''; // TODO @@ -44,15 +44,15 @@ export class ProductIconThemeData implements IWorkbenchProductIconTheme { this.isLoaded = false; } - public ensureLoaded(fileService: IFileService, logService: ILogService): Promise<string | undefined> { + public ensureLoaded(fileService: IExtensionResourceLoaderService, logService: ILogService): Promise<string | undefined> { return !this.isLoaded ? this.load(fileService, logService) : Promise.resolve(this.styleSheetContent); } - public reload(fileService: IFileService, logService: ILogService): Promise<string | undefined> { + public reload(fileService: IExtensionResourceLoaderService, logService: ILogService): Promise<string | undefined> { return this.load(fileService, logService); } - private load(fileService: IFileService, logService: ILogService): Promise<string | undefined> { + private load(fileService: IExtensionResourceLoaderService, logService: ILogService): Promise<string | undefined> { const location = this.location; if (!location) { return Promise.resolve(this.styleSheetContent); @@ -168,10 +168,10 @@ interface ProductIconThemeDocument { fonts: FontDefinition[]; } -function _loadProductIconThemeDocument(fileService: IFileService, location: URI): Promise<ProductIconThemeDocument> { - return fileService.readFile(location).then((content) => { +function _loadProductIconThemeDocument(fileService: IExtensionResourceLoaderService, location: URI): Promise<ProductIconThemeDocument> { + return fileService.readExtensionResource(location).then((content) => { let errors: Json.ParseError[] = []; - let contentValue = Json.parse(content.value.toString(), errors); + let contentValue = Json.parse(content, errors); if (errors.length > 0) { return Promise.reject(new Error(nls.localize('error.cannotparseicontheme', "Problems parsing product icons file: {0}", errors.map(e => getParseErrorMessage(e.error)).join(', ')))); } else if (Json.getNodeType(contentValue) !== 'object') { diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index a04be9102b6..f7876df25a6 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -39,6 +39,7 @@ import { IHostColorSchemeService } from 'vs/workbench/services/themes/common/hos import { RunOnceScheduler, Sequencer } from 'vs/base/common/async'; import { IUserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; import { getIconsStyleSheet } from 'vs/platform/theme/browser/iconsStyleSheet'; +import { asCssVariableName, getColorRegistry } from 'vs/platform/theme/common/colorRegistry'; // implementation @@ -108,7 +109,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { @IConfigurationService private readonly configurationService: IConfigurationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IWorkbenchEnvironmentService readonly environmentService: IWorkbenchEnvironmentService, - @IFileService private readonly fileService: IFileService, + @IFileService fileService: IFileService, @IExtensionResourceLoaderService private readonly extensionResourceLoaderService: IExtensionResourceLoaderService, @IWorkbenchLayoutService readonly layoutService: IWorkbenchLayoutService, @ILogService private readonly logService: ILogService, @@ -476,6 +477,16 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { }; ruleCollector.addRule(`.monaco-workbench { forced-color-adjust: none; }`); themingRegistry.getThemingParticipants().forEach(p => p(themeData, ruleCollector, this.environmentService)); + + const colorVariables: string[] = []; + for (const item of getColorRegistry().getColors()) { + const color = themeData.getColor(item.id, true); + if (color) { + colorVariables.push(`${asCssVariableName(item.id)}: ${color.toString()};`); + } + } + ruleCollector.addRule(`.monaco-workbench { ${colorVariables.join('\n')} }`); + _applyRules([...cssRules].join('\n'), colorThemeRulesClassName); } @@ -564,7 +575,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { if (iconTheme !== this.currentFileIconTheme.id || !this.currentFileIconTheme.isLoaded) { const newThemeData = this.fileIconThemeRegistry.findThemeById(iconTheme) || FileIconThemeData.noIconTheme; - await newThemeData.ensureLoaded(this.fileService); + await newThemeData.ensureLoaded(this.extensionResourceLoaderService); this.applyAndSetFileIconTheme(newThemeData); // updates this.currentFileIconTheme } @@ -583,7 +594,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { private async reloadCurrentFileIconTheme() { return this.fileIconThemeSequencer.queue(async () => { - await this.currentFileIconTheme.reload(this.fileService); + await this.currentFileIconTheme.reload(this.extensionResourceLoaderService); this.applyAndSetFileIconTheme(this.currentFileIconTheme); }); } @@ -639,7 +650,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { iconTheme = iconTheme || ''; if (iconTheme !== this.currentProductIconTheme.id || !this.currentProductIconTheme.isLoaded) { const newThemeData = this.productIconThemeRegistry.findThemeById(iconTheme) || ProductIconThemeData.defaultTheme; - await newThemeData.ensureLoaded(this.fileService, this.logService); + await newThemeData.ensureLoaded(this.extensionResourceLoaderService, this.logService); this.applyAndSetProductIconTheme(newThemeData); // updates this.currentProductIconTheme } @@ -657,7 +668,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { private async reloadCurrentProductIconTheme() { return this.productIconThemeSequencer.queue(async () => { - await this.currentProductIconTheme.reload(this.fileService, this.logService); + await this.currentProductIconTheme.reload(this.extensionResourceLoaderService, this.logService); this.applyAndSetProductIconTheme(this.currentProductIconTheme); }); } diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index 12e377d5510..667b02954c4 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -195,9 +195,9 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat this.updateAuthenticationProviders(); const allAccounts: Map<string, UserDataSyncAccount[]> = new Map<string, UserDataSyncAccount[]>(); - for (const { id } of this.authenticationProviders) { + for (const { id, scopes } of this.authenticationProviders) { this.logService.trace('Settings Sync: Getting accounts for', id); - const accounts = await this.getAccounts(id); + const accounts = await this.getAccounts(id, scopes); allAccounts.set(id, accounts); this.logService.trace('Settings Sync: Updated accounts for', id); } @@ -208,11 +208,11 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat this.updateAccountStatus(current ? AccountStatus.Available : AccountStatus.Unavailable); } - private async getAccounts(authenticationProviderId: string): Promise<UserDataSyncAccount[]> { + private async getAccounts(authenticationProviderId: string, scopes: string[]): Promise<UserDataSyncAccount[]> { let accounts: Map<string, UserDataSyncAccount> = new Map<string, UserDataSyncAccount>(); let currentAccount: UserDataSyncAccount | null = null; - const sessions = await this.authenticationService.getSessions(authenticationProviderId) || []; + const sessions = await this.authenticationService.getSessions(authenticationProviderId, scopes) || []; for (const session of sessions) { const account: UserDataSyncAccount = new UserDataSyncAccount(authenticationProviderId, session); accounts.set(account.accountName, account); diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index d6fa0765f57..0fc9057ded0 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -20,6 +20,7 @@ import { ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, is import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { isEqualAuthority } from 'vs/base/common/resources'; export const WORKSPACE_TRUST_ENABLED = 'security.workspace.trust.enabled'; export const WORKSPACE_TRUST_STARTUP_PROMPT = 'security.workspace.trust.startupPrompt'; @@ -432,7 +433,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork return false; } - return (getRemoteAuthority(uri) === this._remoteAuthority.authority.authority) && !!this._remoteAuthority.options?.isTrusted; + return (isEqualAuthority(getRemoteAuthority(uri), this._remoteAuthority.authority.authority)) && !!this._remoteAuthority.options?.isTrusted; } private set isTrusted(value: boolean) { diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 62459265ea6..44c58d72571 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -93,7 +93,7 @@ import 'vs/workbench/services/quickinput/browser/quickInputService'; import 'vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService'; import 'vs/workbench/services/authentication/browser/authenticationService'; import 'vs/workbench/services/hover/browser/hoverService'; -import 'vs/workbench/services/experiment/common/experimentService'; +import 'vs/workbench/services/assignment/common/assignmentService'; import 'vs/workbench/services/outline/browser/outlineService'; import 'vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl'; diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index e71e108af60..57c2caf0a27 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -59,6 +59,7 @@ import 'vs/workbench/electron-browser/desktop.main'; import 'vs/workbench/services/search/electron-browser/searchService'; import 'vs/workbench/services/extensions/electron-browser/extensionService'; +import 'vs/platform/extensions/node/extensionHostStarter'; // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! diff --git a/src/vs/workbench/workbench.desktop.sandbox.main.ts b/src/vs/workbench/workbench.desktop.sandbox.main.ts index d7c7c3928ae..9279adf4e22 100644 --- a/src/vs/workbench/workbench.desktop.sandbox.main.ts +++ b/src/vs/workbench/workbench.desktop.sandbox.main.ts @@ -33,6 +33,7 @@ import 'vs/workbench/electron-sandbox/desktop.main'; //#region --- workbench services +import 'vs/workbench/services/extensions/electron-sandbox/extensionHostStarter'; //#endregion diff --git a/src/vs/workbench/workbench.sandbox.main.ts b/src/vs/workbench/workbench.sandbox.main.ts index 03bdea9dc09..74e73df9c16 100644 --- a/src/vs/workbench/workbench.sandbox.main.ts +++ b/src/vs/workbench/workbench.sandbox.main.ts @@ -52,7 +52,6 @@ import 'vs/workbench/services/credentials/electron-sandbox/credentialsService'; import 'vs/workbench/services/encryption/electron-sandbox/encryptionService'; import 'vs/workbench/services/localizations/electron-sandbox/localizationsService'; import 'vs/workbench/services/telemetry/electron-sandbox/telemetryService'; -import 'vs/workbench/services/extensions/electron-sandbox/extensionHostStarter'; import 'vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService'; import 'vs/workbench/services/extensionManagement/electron-sandbox/extensionTipsService'; import 'vs/workbench/services/userDataSync/electron-sandbox/userDataSyncMachinesService'; diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index 953607b836f..0f8c28093d7 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -366,7 +366,7 @@ interface IWorkbenchConstructionOptions { /** * [TEMPORARY]: This will be removed soon. * Use an unique origin for the web worker extension host. - * Defaults to false. + * Defaults to true. */ readonly __uniqueWebWorkerExtensionHostOrigin?: boolean; diff --git a/test/automation/src/editor.ts b/test/automation/src/editor.ts index dd564c9bd52..a3589724e3a 100644 --- a/test/automation/src/editor.ts +++ b/test/automation/src/editor.ts @@ -51,15 +51,6 @@ export class Editor { return peek; } - async waitForHighlightingLine(filename: string, line: number): Promise<void> { - const currentLineIndex = await this.getViewLineIndex(filename, line); - if (currentLineIndex) { - await this.code.waitForElement(`.monaco-editor .view-overlays>:nth-child(${currentLineIndex}) .current-line`); - return; - } - throw new Error('Cannot find line ' + line); - } - private async getSelector(filename: string, term: string, line: number): Promise<string> { const lineIndex = await this.getViewLineIndex(filename, line); const classNames = await this.getClassSelectors(filename, term, lineIndex); diff --git a/test/smoke/src/areas/statusbar/statusbar.test.ts b/test/smoke/src/areas/statusbar/statusbar.test.ts index 954cefedcc3..b59d8c3ce50 100644 --- a/test/smoke/src/areas/statusbar/statusbar.test.ts +++ b/test/smoke/src/areas/statusbar/statusbar.test.ts @@ -65,18 +65,6 @@ export function setup(opts: minimist.ParsedArgs) { await app.workbench.problems.waitForProblemsView(); }); - it(`checks if 'Go to Line' works if called from the status bar`, async function () { - const app = this.app as Application; - - await app.workbench.quickaccess.openFile('app.js'); - await app.workbench.statusbar.clickOn(StatusBarElement.SELECTION_STATUS); - - await app.workbench.quickinput.waitForQuickInputOpened(); - - await app.workbench.quickinput.submit(':15'); - await app.workbench.editor.waitForHighlightingLine('app.js', 15); - }); - it(`verifies if changing EOL is reflected in the status bar`, async function () { const app = this.app as Application; diff --git a/yarn.lock b/yarn.lock index 99c26482066..9eead100e6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -492,13 +492,6 @@ dependencies: applicationinsights "*" -"@types/chokidar@2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@types/chokidar/-/chokidar-2.1.3.tgz#123ab795dba6d89be04bf076e6aecaf8620db674" - integrity sha512-6qK3xoLLAhQVTucQGHTySwOVA1crHRXnJeLwqK6KIFkkKa2aoMFXh+WEi8PotxDtvN6MQJLyYN9ag9P6NLV81w== - dependencies: - chokidar "*" - "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" @@ -2163,21 +2156,6 @@ charenc@~0.0.1: resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= -chokidar@*: - version "3.2.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.2.3.tgz#b9270a565d14f02f6bfdd537a6a2bbf5549b8c8c" - integrity sha512-GtrxGuRf6bzHQmXWRepvsGnXpkQkVU+D2/9a7dAe4a7v1NhrfZOZ2oKf76M3nOs46fFYL8D+Q8JYA4GYeJ8Cjw== - 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" - chokidar@3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b" @@ -4358,11 +4336,6 @@ fsevents@^1.2.7: bindings "^1.5.0" nan "^2.12.1" -fsevents@~2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.1.tgz#74c64e21df71721845d0c44fe54b7f56b82995a9" - integrity sha512-4FRPXWETxtigtJW/gxzEDsX1LVbPAM93VleB83kZB+ellqbHMkyt2aJfuzNLRvFPnGi6bcE5SvfxgbXPeKteJw== - fsevents@~2.1.2: version "2.1.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" @@ -4678,16 +4651,21 @@ got@^9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -graceful-fs@4.2.6, graceful-fs@^4.1.2, graceful-fs@^4.2.4: - version "4.2.6" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" - integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== +graceful-fs@4.2.8: + version "4.2.8" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" + integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, 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.2, graceful-fs@^4.2.4: + version "4.2.6" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" + integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== + growl@1.10.5: version "1.10.5" resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" @@ -6885,10 +6863,10 @@ native-is-elevated@0.4.3: resolved "https://registry.yarnpkg.com/native-is-elevated/-/native-is-elevated-0.4.3.tgz#f1071c4a821acc71d43f36ff8051d3816d832e1c" integrity sha512-bHS3sCoh+raqFGIxmL/plER3eBQ+IEBy4RH/4uahhToZneTvqNKQrL0PgOTtnpL55XjBd3dy0pNtZMkCk0J48g== -native-keymap@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/native-keymap/-/native-keymap-3.0.0.tgz#1228f22a4c6c9cf25b25fa8b0f67ee2ad2ee00fc" - integrity sha512-J2IoYW6ICqg5URxfVMa0bdCo2mLJhXoI6RQmeGtfE2Tv2dOpYvT4FnU11dJd/PB5V91QroAOwWQf2ngpeKm5/g== +native-keymap@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/native-keymap/-/native-keymap-3.0.1.tgz#7cc2d30da1e60cbb7d599423e05cb84845d20a8f" + integrity sha512-IeHaz5NM1mF3AKIwBxf4YhgrB/hcctVwIqOXaMrR8Hz8v45dCa364YDvEN0004zSycRyhrXh6cNgCQ/v6QUHkA== native-watchdog@1.3.0: version "1.3.0" @@ -8400,13 +8378,6 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" -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" - readdirp@~3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" @@ -10032,10 +10003,10 @@ typescript@^2.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" integrity sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q= -typescript@^4.5.0-dev.20211021: - version "4.5.0-dev.20211021" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.0-dev.20211021.tgz#7bd786f53c33f7ade525d0b34bda27f2e01927fe" - integrity sha512-lZJTNSL8CBqgURSrVwCto8WEV1U0BUwoy+GTmINoRc/Sm2BcYiRpcq/OAiSg/1q/rvODousdOsHePfAZfwi0Qg== +typescript@^4.5.0-dev.20211029: + version "4.5.0-dev.20211029" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.0-dev.20211029.tgz#ec4619ab136bd70ddd9ec1a7c18783b7ce9990a3" + integrity sha512-N+2wLMbTq+jQmad78i4wKBGcXudBFWy+QdV1Xu9cx+F5Xi6hubBotFEzS7zA7G1Eevy6NJwlsNy0G8ok2GQ9nw== typical@^4.0.0: version "4.0.0" @@ -10403,10 +10374,10 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== -vscode-debugprotocol@1.48.0: - version "1.48.0" - resolved "https://registry.yarnpkg.com/vscode-debugprotocol/-/vscode-debugprotocol-1.48.0.tgz#6af8e3726572ff1716308e8fe50775882074ab12" - integrity sha512-l2jtStdGHAca+B/ZmGAeYZtx7NHT9SY9LL6g0QeImKZjQ9P7S6wB2lcBwz1LSkgFltwLw197yFULnCr36OkLKA== +vscode-debugprotocol@1.50.0: + version "1.50.0" + resolved "https://registry.yarnpkg.com/vscode-debugprotocol/-/vscode-debugprotocol-1.50.0.tgz#6f3e1204d3f4439afb4a5dc9d5396ecef0a94245" + integrity sha512-kPC8LC0rwkKMjWnbDmffoNKKhoNt40ZaeJGXFBAT/KmBX38M71EG5J5uosaqvHTIWkucKXAw1azh8doydxwyZw== vscode-nls-dev@^3.3.1: version "3.3.1" @@ -10483,10 +10454,10 @@ vscode-telemetry-extractor@^1.8.0: ts-morph "^11.0.0" vscode-ripgrep "^1.11.3" -vscode-textmate@5.4.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.4.0.tgz#4b25ffc1f14ac3a90faf9a388c67a01d24257cd7" - integrity sha512-c0Q4zYZkcLizeYJ3hNyaVUM2AA8KDhNCA3JvXY8CeZSJuBdAy3bAvSbv46RClC4P3dSO9BdwhnKEx2zOo6vP/w== +vscode-textmate@5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.4.1.tgz#09d566724fc76b60b3ad9791eebf1f0b50f29e5a" + integrity sha512-4CvPHmfuZQaXrcCpathdh6jo7myuR+MU8BvscgQADuponpbqfmu2rwTOtCXhGwwEgStvJF8V4s9FwMKRVLNmKQ== vscode-windows-ca-certs@^0.3.0: version "0.3.0"